文章目录
- 问题提出
- 一、结构体
- 1.1结构体的声明
- 1.1.1正常定义的结构体
- 1.1.2在声明结构体的同时声明变量
- 1.1.3typedef
- 1.1.4成员变量
- 1.2结构体成员变量的使用
- 1.2.1成员运算符 .
- 1.2.2成员运算符 ->
- 1.3内存对齐
- 1.3.1什么是内存对齐
- 1.3.2内存对齐原则
- 1.3.3结构体成员的定义顺序
- 1.4C++中结构体的声明
- 二、类
- 2.1类的定义
- 2.2类成员的可见性
- 2.3构造和析构函数
- 2.3.1构造函数
- 2.3.2析构函数
- 三、类成员
- 3.1this指针
- 3.2静态成员
- 3.3编译器和运行期
- 3.4常量
- 3.5常函数
- 3.6常量对象
- 3.7内联
- 四、类之间的横向关系
- 4.1组合
- 4.2依赖
- 4.3关联
- 4.4 聚合
- 五、类之间的纵向关系
- 5.1继承
- 5.2继承方式
- 5.3继承下构造析构的执行顺序
- 5.4父类的指针指向子类的对象
- 六、C++结构体和C语言结构体的区别
- 七、C++结构体和类的区别
🐱🚀个人博客https://blog.csdn.net/qq_51000584/
🐱👤收录专栏:C++知识分享
🐱👓专栏目标:整理一些详细的C++知识分享
🐱💻作者:敲代码的猫(Codemon)
问题提出
我们常见的数据类型有整型、浮点型、字符型等等,他们可以满足我们对像成绩、金额、姓名等数据成员的描述。如下:
int grade;//成绩
double amount;//金额
char name[16];//姓名
但是当我们需要对一个事物的整体信息详细的描述时,就无法用一个单一的上述变量满足我们的需求。例如要描述一名学生和他的信息(学号,姓名,性别,班级),也许我们会考虑将所有信息分开定义:
//学生信息----
int _uid;//学号
char _name[16];//姓名
int _sex;//性别
int _class;//班级
//-----------
这样暂时可以满足我们对一名学生信息的记录,但是当我们有多名学生时该如何记录呢?数组或许可以帮助我们解决这个问题:
const int N = 10;//学生数量
int _uid[N];//学号
char _name[16*N];//姓名
int _sex[N];//性别
int _class[N];//班级
当我们取出每名学生信息并打印时,要做如下操作:
for(int i = 0;i < 10; ++ i){
printf("student id:%d\n",_uid[i]);
printf("student name:");
for(int j = i*16;j < (i+1)*16;j++){
printf("%c",_name[j]);
}
printf("\n");
printf("student sex:%d\n",_sex[i]);
printf("student class:%d\n",_class[i]);
}
可见对于每个人都需要数组存储的姓名这一信息,在多个学生信息取出时分割起来是非常复杂的。如果使用多个数组来存储信息(_name_0,_name_1,_name_2…),在使用时寻找是哪个数组也很复杂。
于是引发我们思考,有没有一种办法可以让多个数据类型来描述通同一个事物呢?
一、结构体
为了使多个数据类型整合为一个整体来描述同一事物,出现了结构体这种数据类型——struct
1.1结构体的声明
像其他数据类型一样(int,long,float,double,char…),结构体也有自己的类型名struct,声明方法如下:
struct 结构体名{
成员变量1
成员变量2
...
};
需要注意的是,在花括号后要加上一个分号代表语句的结束,就像int a;后面的分号一样。现在我们描述学生信息(学号,姓名,性别,班级)只需要这样定义:
1.1.1正常定义的结构体
struct studentInfo{
int _uid;//学号
char _name[16];//姓名
int _sex;//性别
int _class;//班级
};
struct studentInfo s;//定义结构体变量
1.1.2在声明结构体的同时声明变量
struct studentInfo{
int _uid;//学号
char _name[16];//姓名
int _sex;//性别
int _class;//班级
}s1,s2;
//上述代码等价于
struct studentInfo{
int _uid;//学号
char _name[16];//姓名
int _sex;//性别
int _class;//班级
};
struct studentInfo s1,s2;
1.1.3typedef
typedef的作用是为数据类型起别名,如:
typedef int INT;
//此时INT可作为int类型使用
INT a = 10;
在结构体中同样可以使用
typedef struct studentInfo{
int _uid;//学号
char _name[16];//姓名
int _sex;//性别
int _class;//班级
}s1,s2;
//以下三者没有区别
s1 stu1;
s2 stu2;
studentInfo stu3;
1.1.4成员变量
在上述结构体中,_uid,_name,_sex,_class我们称为结构体的成员变量。
1.2结构体成员变量的使用
要使用结构体的成员变量,我们需要用到成员运算符,首先定义结构体:
struct studentInfo{
int _uid;//学号
char _name[16];//姓名
int _sex;//性别
int _class;//班级
};
struct studentInfo s;//定义结构体变量
1.2.1成员运算符 .
现在我们通过成员运算符**.**来为s的成员变量赋值:
s._uid = 10;
sprintf(s._name,"Lihua");
s._sex = 1;//代表男
s._class = 2;
假设我们有结构体数组struct studentInfo s[10];并且都以初始化,现在我们要取出每名学生信息并打印只需:
for(int i = 0; i < 10 ;i ++){
printf("student id:%d\n",s[i]._uid);
printf("student name:%s\n",s[i]._name);
printf("student sex:%d\n",s[i]._sex);
printf("student class:%d\n",s[i]._class);
}
1.2.2成员运算符 ->
和.运算符一样,->也是成员运算符,但->是使用场景是指针变量的成员,如:
struct studentInfo{
int _uid;//学号
char _name[16];//姓名
int _sex;//性别
int _class;//班级
};
struct studentInfo *s = (studentInfo*)malloc(sizeof(studentInfo));
//此时s是一个已分配空间的指针变量,为其成员初始化如下:
s->_uid = 10;
sprintf(s->_name,"Lihua");
s->_sex = 1;//代表男
s->_class = 2;
...
free(s);//回收资源
在使用上->与.没有区别,成员运算符描述了成员变量的归属,如s1._uid与s2._uid是不同的,这样便于我们区分。
1.3内存对齐
1.3.1什么是内存对齐
在C语言中,结构体是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构体、联合体等)的数据单元。在结构体中,编译器为结构体的每个成员按其自然边界分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构体的地址相同。
为了使CPU能够对变量进行快速的访问,变量的起始地址应该具有某些特性,即所谓的“对齐”,比如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除,也即“对齐”跟数据在内存中的位置有关。如果一个变量的内存地址正好位于它长度的整数倍,他就被称做自然对齐。为什么要字节对齐?为了快速准确的访问,若没有字节对齐则会出现多次访问浪费时间。举例说明(定义一个char 和 int型数据不按照字节对齐存储的情况需要多次访问)
1.3.2内存对齐原则
1.结构体的数据成员第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小的整数倍开始。
2.结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部"最宽基本类型成员"的整数倍地址开始存储。
如:
struct b{
char s1;//1字节
int s2;//4字节
double s3;//8字节
};
struct a{
struct b temp;
};//struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储
3.收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的"最宽基本类型成员"的整数倍。不足的要补齐。(基本类型不包括struct/class/uinon)。
4.sizeof(union),以结构里面size最大元素为union的size,因为在某一时刻,union只有一个成员真正存储于该地址。//本条性质适用于联合体
1.3.3结构体成员的定义顺序
几个实例:
struct b {
char s1;//1字节
int s2;//4字节
double s3;//8字节
};
void main(){
cout<<sizeof(b)<<endl;
}
输出的结果为16;
解析:
s1为结构体的第一个成员变量,放在起始位置offset为0的地方。s2为结构体第二个成员变量,大小为4字节,此时s2需放在4的整数倍大小的位置 ,因此s2从第四个字节开始存储。s3为结构体第三个成员变量,其大小为8字节,s3需放在8的整数倍大小的位置,也就是从8开始存储。
struct b {
int s2;//4字节
double s3;//8字节
char s1;//1字节
};
void main(){
cout<<sizeof(b)<<endl;
}
同样的定义,我们更改了s1的定义顺序,此时输出结果为24;
解析:
s2为结构体的第一个成员变量,放在起始位置offset为0的地方。s3为结构体第二个成员变量,大小为8字节,此时s3需放在8的整数倍大小的位置 ,因此s3从第8个字节开始存储。s1为结构体第三个成员变量,其大小为1字节,但此时最宽的基本类型成员是double,因此s1需放在8的整数倍大小的位置,也就是从16开始存储。
可见,结构体成员的声明先后顺序会影响其所占内存大小。
1.4C++中结构体的声明
C++中我们可以直接使用结构的名作为一种类型名来声明变量,如:
struct student{
int _uid;
int _class;
char _name[16];
};
student s1;
s1._uid = 10;
s1._class = 1;
两种定义方式
struct student{
int _uid;
int _class;
char _name[16];
}s1;
//-----------------------
struct student{
int _uid;
int _class;
char _name[16];
};
student s1;
//------------------------
//以上两种定义形式没有区别
初始化方式
struct student{
int _uid;
int _class;
char _name[16];
};
//成员数据初始化
student s1 = { 10 , 2, "Lihua"};
二、类
类似结构体,类是一种用户定义数据类型。一个类是一组具有共同属性和行为的对象的抽象描述。面向对象的程序就是一组类构成的。一个类中描述了一组数据来表示其属性,以及操作这些数据的一组函数作为其行为。
与C语言中的结构体不同的是,类是为了实现C++面向对象编程,具有封装性、继承性、多态性的特性。类不光可以描述一件事物的性质,甚至可以描述事物的一系列操作。但是C++中的结构体也具有类的一些性质,后面我们会分开介绍。
2.1类的定义
定义一个类就是描述其类名及其成员。对于成员,还要描述各成员的可见性。
如何定义一个类?习惯上将一个类的定义分为两个部分:说明部分和实现部分。说明部分包括类中包含的数据成员和成员函数的原型,实现部分描述各成员函数的具体实现。
如:
//类的声明部分
class 类名{
private:
//私有成员
protected:
//保护成员
public:
//公有成员
};//同样这里要用分号
//类的实现部分
...
class是说明类的关键字,类名是一个标识符,一对花括号表示类的作用域范围,其后的分号表示类定义结束。
关键字public、private和protected称为访问控制修饰符,描述了类成员的可见性。每个成员都有唯一的可见性。
一个类中的成员没有前后次序,但最好把所有成员都按照其可见性放在一起。私有成员组成一组,保护成员组成一组,公有成员再组成一组。这三组之间没有次序要求,而且每一组内的多个成员之间也没有次序要求。一个类不一定都具有这三组成员。
成员函数的实现既可以在类体内描述,也可以在类体外描述。如果一个成员函数在类体内描述,就不用再出现在类外的实现部分。如果所有的成员函数都在类体内实现,就可以省略类外的实现部分。
在类体外实现的函数必须说明它所属的类名,如:
返回值 类名::函数名(参数){}
//例:
class book{
public:
void set(int a);//次数只声明,不实现
};
void book::set(int a){
...
}
2.2类成员的可见性
C++提供了3种访问控制修饰符:private(私有)、protected(保护)和public(公有)。每个成员只能选择其中之一。
-私有成员 只允许本类的成员函数来访问,对类外部不可见。数据成员往往作为私有成员。
-保护成员 能被自己类的成员函数访问,也能被自己的派生类访问,但其它类不能访问。
-公有成员 对类外可见。当然类内部也能访问。公有成员作为该类对象的操作接口,使类外部程序能操作对象。成员函数一般作为公有成员。
class student{
private:
int a;
void set(int _a){
a = _a;
}
public:
int b;
};
main(){
student s;
s.b = 10;//访问类中公有成员变量
cout << s.a<<endl;//不可访问,将会报错
}
至此,我们可以用类来定义一个学生对象,此时学生对象中不光可以包含学生的信息,还可以对学生信息进行一系列操作,如将学生的信息打印,判断学生的成绩是否及格:
class student{
public:
int _uid;
int _class;
char _name[16];
int _sex;
int _grade;//成绩
public:
void PrintInfo(){//输出学生信息
cout<<"uid:"<<uid<<endl;
cout<<"class:"<<_class<<endl;
cout<<"name:"<<_name<<endl;
cout<<"sex:"<<_sex<<endl;
}
bool isQualified(){//判断成绩是否合格
if(_grade >= 60){
return true;
}else{
return false;
}
}
};
在访问时,仍使用成员运算符:
student t;
t.grade = 65;//t同学初始化成绩
cout<<t.isQualified()<<endl;//输出t同学是否及格
2.3构造和析构函数
2.3.1构造函数
每个类都有构造函数,由系统调用,用于创建该类的对象并进行初始化。
构造函数是一种特殊的函数,其作用是在创建对象时,由系统来调用,对新建对象的状态进行初始化。
构造函数的特殊性有以下几点:
(1)构造函数的名字必须与类名相同。
(2)构造函数不指定返回值类型。
(3)构造函数可以无参,也可有多个形参,因此一个类中可以重载定义多个构造函数。
(4)创建一个对象时,系统会根据实参来自动调用某个构造函数。
创建对象至少包括以下三种情形:
(1)说明一个对象变量或数组,也包括函数形参对象。
(2)使用new运算符动态创建对象。
(3)创建临时匿名对象,后面介绍。
构造函数一般是公有的,使类外程序能按形参要求来创建对象。在特定情况下,构造函数也可能作为私有,以限制外部程序随意创建对象。
实例:
class A{
public:
int a;
};
A a;
此时类A没有对构造函数的定义,系统会默认为其分配一个无参的构造函数,函数体是空的
class A{
public:
int a;
A(int _a){this->a = _a;}//定义了构造函数,系统将不再默认分配无参构造
};
A a;//出错
此时程序会出错,因为类A没有对无参构造函数的定义,只有有参数构造函数,因此创建对象时必须要传参
class A{
public:
int a;
A(){}//-1
A(int _a){this->a = _a;}//-2
A(int _a,int _b){this->a = _a; _b = this->a-1;}//-3
};
A a;//此时调用-1的构造函数
构造函数可以被重载。
2.3.2析构函数
析构函数(destructor)与构造函数的作用正相反,它用来完成对象被撤销前的扫尾清理工作。
析构函数是在撤消对象前由系统自动调用的,析构函数执行后,系统回收该对象的存储空间,该对象的生命周期也就结束了。
析构函数是类中的一种特殊的函数,它具有以下特性:
(1)析构函数名是在类名前加“~”构成。该符号曾作为按位求反的单目运算符。
(2)不指定返回类型。
(3)析构函数没有形参,因此也不能被重载定义,即一个类只能有一个析构函数。
(4)在撤销一个对象时,系统将自动调用析构函数,该对象作为析构函数的当前对象。
(5)如果没有显式定义析构函数,编译器将生成一个公有的析构函数,称其为缺省析构函数,函数体为空。
当撤销一个对象时,该对象的类的析构函数将自动执行,而且该对象作为析构函数的当前对象。在以下三种情况下要调用析构函数:
(1)当程序执行离开局部对象所在的作用域时,要撤销局部对象。当程序终止时,要撤销全局对象和静态对象。
(2)用delete运算符回收先前用new创建的对象。
(3)临时匿名对象使用完毕。
class A{
public:
A(){cout<<"A()"<<endl;}
~A(){cout<<"~A()"<<endl;}
}
void main(){
A a;
cout<<---<<endl;
}//当遇到另一半花括号时,代表该局部变量的声明周期结束了
运行结果为
A()
---
~A()
三、类成员
C++标准规定,凡是一个独立的﴾非附属﴿对象都必须具有非零大小,所以一个空类即使没有任何数
据存储其大小不能为0,空类实例占用内存的大小为1,是用来在内存中占位的,不同的对象在内
存中的地址不同。
class A{};
void main(){
cout<<sizeof(A)<<endl;
}
输出结果为1;
内存对齐规则类与结构体相同。
类成员属性,是属于对象的,只有在定义对象的时候,才会真正的存在(在内存中分配空间),定义多个对象,成员属性存在多份(表现为在内存中的地址不同),彼此独立,互不干扰。
类成员函数,是属于类的,在编译期就存在了,与是否定义对象无关。类成员函数只有一份,多个对 象共享同一个函数。
3.1this指针
类中的非静态成员函数包括构造析构,会有一个默认的隐藏的参数——this 指针,它是编译器默认加上的在所有参数之前,类型为当前类的指针 即: 类 * const this,当我们在用对象调用函数的时候,this 指针就指向了调用的对象,在函数中使用类成员属性或其他类成员函数都是默认通过this指针调用的,在平时写代码的时不用显示的指明this,因为编译器会默认加上。
class A{
int a;
void show(/* A* const this */){
a;
this->a;
//二者等价
}
};
A a;
A b;
a.show(/* A* const this */);//this指针传&a
b.show(/* A* const this */);//this指针传&b
3.2静态成员
静态成员需要使用关键字 static 修饰。
static int m_a;
static void show(){}
静态成员变量属于类的,在编译期就存在,不参与类对象的空间占用,可直接类名作用域去调用(对象调用也可以),初始化之前有默认值,需要在类外进行初始化,类型 类名::变量名 = 值,所有的对象共享这个静态变量,类中的普通成员函数可以使用静态成员变量。
静态成员函数属于类的,在编译期就存在,可直接类名作用域去调用(对象调用也可以)。与普通的成员函数的区别是静态成员函数没有隐藏的this指针参数,也就不能使用普通的成员变量,只能使用静态成员变量。
3.3编译器和运行期
编译期是指把源程序交给编译器编译、生成的过程,最终得到可执行文件。运行期是指将可执行文
件交给操作系统执行、直到程序退出,执行的目的是为了实现程序的功能。 类是编译期的概念,包括成员的访问控制和作用域。
对象是运行期的概念,包括定义类的实例、引用、指针等使用其类成员。
3.4常量
当类中有const类型的变量时,在定义的时候必须要初始化,而这个初始化操作是在初始化参数列
表中完成的,而构造函数的函数体代码中进行的操作严格来说是赋值,先执行初始化列表,在执行
构造函数体中的代码。对于普通的变量来说也可在初始化参数列表中初始化。写法:在构造函数的
参数列表后加上一个冒号 : 后面是初始化的成员,用圆括号 ()的形式指定初始化值,多个成员用
逗号,分割。
class A{
public:
int a;
const int b;
char c;
A():a(1),b(3),c('c'){
a = 20;//允许赋值
b = 30;//不允许赋值
}
};
常量的特性:定义就必须初始化,一旦初始化后就不能再去修改其值(不能通过正常手段修改)。
3.5常函数
常函数:类中的成员函数参数列表后面有const修饰时,称之为常函数,其主要作用是为了能够保护类中的成员变量,其特性是:不能修改类中的非静态成员,因为const修饰this指针变为const 类* const this,也就是不能执行 this‐>变量=val 操作,但是仍然可以查看成员变量。对于静态成员属性不但能查看,也能对其修改,因为静态成员可不通过this 去使用。
在常函数中可以查看普通的变量、常量、静态变量等,也可以调用其他常函数,但是却不能使用普通的成员函数,因为其this指针的类型并不相同,CTest* const this = const CTest* const this这将是一个非法的操作。
3.6常量对象
常量对象:使用const 修饰的对象(如 const CTest tst;) 不能调用普通的成员函数,只能调用
常函数。这里面涉及到了 this指针的安全级别升级还是降级的操作。
CTest const this = &const CTest,这是一个安全级别降级的非法的操作,而普通的对象调
用常函数 const CTest const this = &CTest 这是一个安全级别升级的合法操作。
3.7内联
内联函数C++为了提高程序的运行速度所做的一项改进,普通函数和内联函数主要区别不在于编写
方式,而在于C++编译器如何将他们组合到程序中的。编译器将使用相应的函数代码替换到内联函
数的调用处,所以程序无需跳转到另一个位置执行函数体代码,所以会比普通的函数稍快,代价是
需要占用更多的内存,空间换时间的做法。
执行函数之前需要做一些准备工作,要将实参、局部变量、返回地址以及若干寄存器都压入栈中,
然后才能执行函数体中的代码,代码执行完毕后还要将之前压入栈中的数据都出栈。这个过程中涉
及到空间和时间的开销问题,如果函数体的中代码比较多,逻辑也比较复杂,那么执行函数体占用
大部分时间,而函数调用、释放空间过程花费的时间占比很小可以忽略;如果函数体的中代码非常
少,逻辑也非常简单,那么相比于函数体代码的执行时间 函数调用机制所花费的时间就不能忽略
了。
int add(int a,int b){
return a+b;
}
int c = add(1,2);
为了消除函数调用时间的开销,C++提供了一种高效率的方法inline函数:
inline int add(int a,int b){
return a+b;
}
int c = add(1,2); //替换后:int c = 1+2;
注意:
- inline是一种空间换时间的做法,内联在一定程度上能提高函数的执行效率,这并不意味着所有
函数都要成为内联函数,如果函数调用的开销时间远小于函数体代码执行的时间,那么效率提
高的并不多,如果该函数被大量调用时,每一处调用都会复制一份函数体代码,那么将占用更
多的内存会增加,得不偿失。所以一般函数体代码比较长,函数体内出现循环(for、while),
switch等不应为内联函数。 - 并非我们加上 inline关键字, 编译器就一定会把它当做内联函数进行替换。定义 inline 函数只
是程序员对编译器提出的一个建议,而不是强制性的,编译器有自己的判断能力,它会根据具
的情况决定是否把它认为是内联函数。编译器不会把递归函数视为内联函数的。 - 类、结构中在的类内部声明并定义的函数默认为内联函数,如果类中只给出声明,在类外定义
的函数,那么默认不是内联函数,除非我们手动加上 inline 关键字。
class CTest{
public:
int show(){ //默认内联
int a = 1;
return a;
}
};
int main(){
CTest tst;
tst.show(); //内联替换
return 0;
}
四、类之间的横向关系
4.1组合
它是一种 “is a part of” 的关系( 部分与整体)。组合是一个类中包含另一个类对象。相比聚合,
组合是一种强所属关系,组合关系的两个对象往往具有相同的生命周期,被组合的对象是在组合对
象创建的同时或者创建之后创建,在组合对象销毁之前销毁。一般来说被组合对象不能脱离组合对
象独立存在,整体不存在,部分一定不存在。
在C++语法中,通常在组合类中包含被组合类对象来实现组合关系:
class CHand{};
class CPeople{
CHand m_hand; //组合(复合)关系
};
4.2依赖
它是一种 “uses a” 的关系。一个对象的某种行为依赖于另一个类对象,被依赖的对象视为完成某个
功能的工具,并不持有对他的引用,只有在完成某个功能的时候才会用到,而且是必不可少的。依
赖之间是没有生命周期约束关系的。举例:人要完成编程这件事,那么需要用到电脑,电脑作为一
个工具,其他的时候不需要,电脑也不可能作为人的属性而存在(非组合关系),人必须依赖于电
脑才能完成编程这件事。
C++语法中,代码的表现形式为多种,通常将被依赖的对象作为另一类方法的参数的形式实现两个
类之间的依赖关系。
class CComputer{};
class CPeople{
void Code(CComputer *pc) //或:CComputer &pc,
{}
};
4.3关联
它是一种"has a"的关系。关联不是从属关系,而是平等关系,可以拥有对方,但不可占有对方。
完成某个功能与被关联的对象有关,但被关联的对象可有可无。被关联的对象与关联的对象无生命
周期约束关系,被关联对象的生命周期由谁创建就由谁来维护。只要二者同意,可以随时解除关系
或是进行关联,被关联的对象还可以再被别的对象关联,所以关联是可以共享的。举例:人和朋友
的关系,人要完成玩游戏这个功能,没有朋友可以自己玩游戏,如果交到朋友了就可以和朋友一起
玩游戏。
C++语法中,通常在关联的类中定义被关联类对象的指针形式实现两个类之间的关联关系。
class CFriend{};
class CPeople{
CFriend *m_pFriend; //关联关系
};
4.4 聚合
它是一种"owns a"的关系。多个被聚合的对象聚集起来形成一个大的整体,聚合的目的是为了统一
进行管理同类型的对象,聚合是一种弱所属关系,被聚合的对象还可以再被别的对象关联,所以被
聚合对象是可以共享的。虽然是共享的,聚合代表的是一种更亲密的关系,相当于强版本的关联。
C++语法中,通常在聚合类中定义被聚合对象指针的容器。
class CPeople{};
class CFamily{
CPeople* families[10];
};
五、类之间的纵向关系
5.1继承
被继承的类叫做基类(父类),继承的类叫派生类(子类),在派生类类名后面加 : 继承方式 基类
class CFather{};
class CSon:public CFather{};
通过继承关系,子类可以使用父类的成员。如果子类和父类有同名的成员,那么默认使用的是子类
的成员,如果想要使用父类的成员,需要在成员名前加上类名::用于显式的指定区分,
son.m_a; //子类成员
son.CSon::m_a; //子类成员
son.CFather::m_a; //父类成员
子类继承父类,相当于将父类的成员包含到自己的类里,所以定义子类对象所占用的空间大小除了
子类自身的成员还包括父类的成员。成员在内存空间分布为:先父类成员后子类成员,而每个类中
的成员分布与在类中定义的顺序一致。
继承的优点:我们可以将类中的一些功能相近、相似的共同的方法,抽离出来放到单独的一个类
中,并让其继承这个类,那么抽离出来的类就是父类,将来其他类在增加公共的方法时,我只需要
在父类添加一份即可。提高了代码的复用性、扩展性。
5.2继承方式
继承的优点:我们可以将类中的一些功能相近、相似的共同的方法,抽离出来放到单独的一个类
中,并让其继承这个类,那么抽离出来的类就是父类,将来其他类在增加公共的方法时,我只需要
在父类添加一份即可。提高了代码的复用性、扩展性。
继承方式 | 父类中属性 | 子类中的属性 |
---|---|---|
public | public | public |
public | protected | protected |
public | private | 不可访问 |
protected | public | protected |
protected | protected | protected |
protected | private | 不可访问 |
private | public | private |
private | protected | private |
private | private | 不可访问 |
5.3继承下构造析构的执行顺序
定义子类对象时执行顺序:父构造‐>子构造‐>孙构造…| …孙析构‐>子析构‐>父析构。
构造顺序说明:在子类创建对象的时候,执行子类的构造函数(注意这里并不是直接先执行父类的
构造函数),但要先执行子类的构造的初始化列表,在初始化列表中会默认调用父类的无参构造初
始化父类成员,如果父类只有带参数的构造,那么需要在子类的初始化参数列表显示的指定父类的
初始化。这有点像之前说的组合关系形式。
析构顺序说明:子类对象的生命周期结束后,因为是子类所以自动调用子类析构,当析构执行完
了,才会回收对象分配的空间,当然这个空间包含创建的父类的成员,那么回收父类成员前,自动
调用父类的析构。如果是new出来的子类对象,同理。
5.4父类的指针指向子类的对象
对于函数重载而言,我们调用的时候,可以根据参数类型、参数个数,编译器自动区分该具体调用
哪个函数。同样如果在一个类中存在两个同名函数(参数列表不同),那么也可以根据调用者传递
的参数自动的区分执行哪个函数,因为也是一个函数重载的关系。
那对于父类和子类中,如果有同名的函数但是参数列表不同,则不能够自动区分,因为他们之间的
关系并不是函数重载的关系,作用域不同,必须使用 类名:: 去区分到底该调用哪个函数。子类中
的重名函数我们称之为 隐藏。
在继承关系下,允许父类的指针指向子类的对象,但是反过来却不行,这么做的好处是:父类的指
针可以统一多个类的类型,提高代码的复用性、扩展性。
六、C++结构体和C语言结构体的区别
(1)C的结构体内不允许有函数存在,C++允许有内部成员函数,且允许该函数是虚函数。
(2)C的结构体对内部成员变量的访问权限只能是public,而C++允许public,protected,private三种。
(3)C语言的结构体是不可以继承的,C++的结构体可继承。
(4)C 中使用结构体需要加上 struct 关键字,而 C++ 中可以省略 struct 关键字直接使用。
C++ 中的 struct 是对 C 中的 struct 进行了扩充,它们在声明时的区别如下:
C | C++ | |
---|---|---|
成员函数 | 不能有 | 可以有 |
静态成员 | 不能有 | 可以有 |
访问控制 | 默认public,不能修改 | public/private/protected |
成员函数 | 不可以继承 | 可以从类或其他结构体继承 |
成员函数 | 不能直接初始化数据成员 | 可以 |
七、C++结构体和类的区别
前面介绍了结构体类型,只是定义了结构中的数据成员,这是传统C语言的用法。而C++语言的结构类型中也能定义成员函数,就像类一样。结构中也可使用关键字private、public和protected来确定成员的可见性。
结构与类的一个重要区别是类成员的缺省访问权限为私有private,而结构成员的缺省访问权限为公有public。另一个区别是说明一个结构变量可用花括号来初始化,但创建一个对象时则不能用花括号初始化,只能用圆括号带实参来初始化,而这又需要编写构造函数才能实现。
如果只需描述一组相关数据,而不需要描述对这些数据的处理函数,建议使用结构类型,这样更简单。如果大多数据成员和成员函数都是公有的,也适合采用结构。如果要求规范的封装性就应使用类。面向对象设计和编程主要是使用类,C++程序也往往使用结构、枚举等类型,但很少用结构来替代类。
Codmeon2024.02.28