1. 类的定义
1.1 类定义格式
^ class 为定义类的关键字,Stack 为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。类中内容称为类的成员:类中的变量称为类的属性或成员变量:类中的函数称为类的方法或者成员函数。
^ 为了区分成员变量,一般习惯上成员变量会加一个特殊标识,如成员变量前面或者后面加 _ 或者 m 开头,注意C++中这个并不是强制的,只是一些惯例,具体看公司的要求。
^ C++中 struct 也可以定义类,C++兼容C中 struct 的用法,同时 struct 升级成了类,明显的变化是 struct 中可以定义函数,一般情况下我们推荐用class定义类。
^ 定义在类里面的成员函数默认为 inline。
cpp
class Stack
{
public:
// 成员函数
void Init(int n = 4)
{
array = (int*)malloc(sizeof(int) * n);
if (nullptr == array)
{
perror("malloc申请空间失败");
return;
}
capacity = n;
top = 0;
}
void Push(int x)
{
// ...扩容
array[top++] = x;
}
int Top()
{
assert(top > 0);
return array[top - 1];
}
void Destroy()
{
free(array);
array = nullptr;
top = capacity = 0;
}
private:
// 成员变量
int* array;
size_t capacity;
size_t top;
}; // 分号不能省略
cpp
class Date
{
public:
void Intit(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
// 为了区分成员变量,一般习惯上成员变量会加一个特殊标识
// 如 _ 或者 m 开头
int _year;
int _month;
int _day;
};
1.2 访问限定符
^ C++一种实现封装的方式,用类将对象的属性与方法结合在一起,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
^ public 修饰的成员在类外可以直接被访问;protected 和 private 修饰的成员在类外不能直接被访问。
^ 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,如果后面没有访问限定符,作用域就到 } (即类结束)。
1.3 类域
^ 类定义了一个新的作用域,类的所以成员都在类的作用域中,在类体外定义成员时,需要使用作用域操作符(::)指明成员属于哪个类域。
^ 类域影响的是编译的查找规则,下面程序中 Init 如果不指定类域 Stack,那么编译器就把 Init 当成全局函数,那么编译时,找不到 array 等成员的声明/定义在哪里,就会报错。指定类域Stack,就是知道 Init 是成员函数,当前域中找不到的 array 等成员,就会到类域中去查找。
cpp
class Stack
{
public:
// 成员函数
void Init(int n = 4);
private:
// 成员变量
int* array;
size_t capacity;
size_t top;
};
// 声明和定义分离,需要指定类域
void Stack::Init(int n)
{
array = (int*)malloc(sizeof(int) * n);
if (nullptr == array)
{
perror("malloc申请空间失败");
return;
}
capacity = n;
top = 0;
}
2. 实例化
2.1 实例化概念
^ 用类类型在物理内存中创建对象的过程,称为类实例化出对象。
^ 类是对象进行一种抽象描述,是一个模型一样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,用类实例化出对象时,才会分配空间。
^ 一个类可以实例化出多个对象,实例化出的对象,占用实际的物理空间,用来存储类成员变量。打个比方:类实例化出对象就像现实中使用建筑设计图建造房子,类就像是设计图,用设计图修建房子,房子才能住人。同样类就像设计图一样,不能存储数据,实例化出的对象分配物理内存存储数据。
cpp
#include<iostream>
using namespace std;
class Date
{
public :
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
// 这里只是声明,没有开空间
int _year;
int _month;
int _day;
};
int main()
{
// Date类实例化出对象 d1 和 d2
Date d1;
Date d2;
return 0;
}
2.2 对象大小
分析一下类对象中哪些成员呢?类实例化出的每个对象,都有独立的数据空间,所以对象中肯定包含成员变量,那么成员函数是否包含呢?首先函数被编译后是一段指令,对象中没办法存储,这些指令存储在一个单独的区域(代码段),那么对象总非要存储的话,只能是成员函数的指针。在分析一下,对象中是否有存储指针的必要呢?Date实例化d1和d2两个对象,d1和d2都要各自独立的成员变量存储各自的数据,但是d1和d2的成员函数 Init 指针却是一样的,存储在对象中就浪费了。如果Date实例化100个对象,那么成员函数指针就重复存储100次,太浪费了。其实函数指针是不需要存储的,函数指针是一个地址,调用函数被编译成汇编指令call地址,其实编译器在编译链接时,就要找到函数的地址,不是在运行时找,只有动态多态是在运行时找,就需要存储函数地址。
内存对齐规则:
-
第一个成员在与结构体偏移量为0的地址
-
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
-
注意:对齐数 = 编译器默认的一个对齐数与该成员大小的较小值
-
VS中默认的对齐数为 8
-
结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍
-
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
cpp
#include<iostream>
using namespace std;
// 计算⼀下A/B/C实例化的对象是多⼤?
class A
{
public:
void Print()
{
cout << _ch << endl;
}
private:
char _ch;
int _i;
};
class B
{
public:
void Print()
{
//...
}
};
class C
{
};
int main()
{
A a;
B b;
C c;
cout << sizeof(a) << endl;
cout << sizeof(b) << endl;
cout << sizeof(c) << endl;
return 0;
}

上面的程序运行后,我们看到没有成员变量的B和C类对象的大小是1,为什么没有成员变量话要给1个字节呢?因为如果一个字节都不给,怎么表示对象存在过呢!所以这里给1个字节,纯粹是为了占位标识对象存在。
3. this 指针
^ Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用Init和Print函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢?那么这里就要看C++给了一个隐含的this 指针解决这里的问题。
^ 编译器编译后,类的成员函数默认都会在形参第一个位置,增加一个当前类类型的指针,叫做this 指针。如Date类的Init的真实原型为,void Init (Date* const this , int year , int month , int day )
^ 类的成员函数中访问成员变量,本质都是通过this指针访问的,如Init函数中给 _year 赋值,this_year = year。
^ C++规定不能再实参和形参的位置显示的写 this 指针(编译时编译器会处理),但是可以在函数体内显示使用 this 指针。
4. 类的默认成员函数

默认成员函数就是用户没有显示实现,编译器会自动生成的成员函数称为默认成员函数。一个类,我们不写的情况下编译器会默认生成一下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,我们稍微了解一个即可。默认成员函数很重要,也比较复杂,我们要从两个方面去学习:
-
我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求。
-
编译器默认生成的函数不满足我们的需求,我们需要自己实现,那么如何自己实现?
5. 构造函数
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象(我们常使用的局部对象是栈帧创建时,空间就开好了),而是对象实例化时初始化对象。构造函数的本质是替代我们以前Stack和Date类中写的Init函数的功能,构造函数自动调用的特点就完美的替代了Init。
构造函数的特点:
1. 函数名与类名相同。
2. 无返回值。(返回值啥都不需要给,也不需要写 void ,不要纠结,C++规定如此)
3. 对象实例化时系统会自动调用对应的构造函数。
4. 构造函数可以重载。
5. 如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义编译器将不再生成。
6. 无参构造函数,全缺省构造函数,我们不写构造时编译器默认生成的构造函数,都叫做默认构造函数。但是这三个函数有且只有一个存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时存在歧义。要注意很多同学会认为默认构造函数就是编译器默认生成那个叫默认构造,实际上无参构造函数,全缺省构造函数也是默认构造,总结一下就是不传实参就可以调用的构造就叫默认构造。
7. 我们不写,编译器默认生成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要用初始化列表才能解决。
8. 每个成员变量在初始化列表只能出现一次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。
9. 引用成员变量,const 成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进行初始化,否则会编译报错。
10. C++11 直接在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的。
11. 尽量使用初始化列表初始化,因为那些你不在初始化列表初始化的成员也会走初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会使用这个缺省值初始化。如果没有给缺省值,对于没有显示在初始化的内置类型成员是否取决于编译器,C++并没有规定。对于没有显示在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构造会编译报错。
12. 初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的先后顺序无关。建议声明顺序和初始化列表顺序保持一致。
说明:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的原生数据类型,如 int / char / double / 指针等,自定义类型就是我们使用 class / struct等关键字自己定义的类型。
初始化列表总结:
无论是否显示写初始化列表,每个构造函数都有初始化列表:
无论是否在初始化列表显示初始化成员变量,每个成员变量都要走初始化列表初始化。

cpp
#include<iostream>
using namespace std;
class Date
{
public:
// 1. 无参构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
// 2. 带参构造函数
Date(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
// 3. 全缺省构造函数
/*Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}*/
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1; // 调用默认构造函数
Date d2(2026,9,16); // 调用带参的构造函数
// 注意:如果通过无参构造函数的创建对象时,对象后面不用跟括号
// 区分这里是函数声明还是实例化对象
// warning C4930: "Date d3(void)": 未调用原型函数(是否是有意用变量定义的?)
Date d3();
d1.Print();
d2.Print();
return 0;
}
6. 析构函数
析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。析构函数的功能类比我们之前 Stack 实现的 Destroy 功能,而像 Date 没有 Destroy ,其实就是没有资源需要释放,所以严格说 Date 是不需要析构函数的。
析构函数的特点:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值。(这里跟构造类似,也不需要加 void )
3. 一个类只能有一个析构函数;若未显示定义,系统会自动生成默认的析构函数。
4. 对象生命周期结束时,系统会自动调用析构函数。
5. 跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,自定义类型成员会调用他的析构函数。
6. 还需要注意的是我们显示写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数。
7. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,如Date;如果默认生成的析构就可以用,也就不需要显示写析构,如MyQueue; 但是有资源申请时,一定要自己写析构,否则会造成资源泄露,如 Stack。
8. 一个局部域的多个对象,C++规定后定义的先析构。
7. 拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则次构造函数也叫做拷贝构造函数,也就是拷贝构造是一个特殊的构造函数。
拷贝构造的特点:
1. 拷贝构造函数是构造函数的一个重载。
2. 拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。拷贝构造函数也可以多个参数,但是第一个参数必须是类类型对象的引用,后面的参数必须有缺省值。
3. C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
4. 若未显示定义拷贝构造,编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝 / 浅拷贝(一个字节一个字节的拷贝)。对自定义类型成员变量会调用他的拷贝构造。
5. 像 Date 这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显示实现拷贝构造。像 Stack 这样的类,虽然也都是内置类型,但是指向了资源,编译器自动生成的拷贝构造完成的值拷贝 / 浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像 MyQueue 这样的类型内部主要是自定义类型 Stack 成员,编译器自动生成的拷贝构造会调用 Stack 的拷贝构造,也不需要我们显示实现。这里还有一个技巧,如果一个类显示实现了析构并释放资源,那么他就需要显示写拷贝构造,否则就不需要。
6. 传值返回会产生一个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用,类似一个野指针一样。传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束还在,才能用引用返回。
8. 赋值运算符重载
8.1 运算符重载
^ 当运算符被用于类类型的对象时,C++语言允许我们通过运算符冲重载的形式指定新的含义。C++规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。
^ 运算符重载是具有特殊名字的函数,他的名字是由 operator 和后面要定义的运算符共同构成。和其他函数一样,他也具有其返回类型和参数列表以及函数体。
^ 重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。
^ 如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的 this 指针,因此运算符重载作为成员函数时,参数比运算对象小一个。
^ 运算符重载以后,其优先级和结核性与对应的内置类型运算符保持一致。
^ 不能通过连接语法中没有的符号来创建新的操作符:比如 operator@。
^
注意以上5个运算符不能重载。
^ 重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义。
^ 一个类需要重载哪些运算符,是看哪些运算符重载后有意义。
^ 重载++运算符时,有前置++和后置++,运算符重载函数名都是 operator++,无法很好的区分。C++规定,后置++重载时,增加一个int形参,跟前置++构成函数重载,方便区分。
^ 重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this 指针默认抢占了第一个形参位置,第一个形参位置是左侧运算对象,调用时就变成了 对象<<cout,不符合使用习惯和可读性。重载为全局函数把 ostream / istream 放到第一个形参位置就可以了,第二个形参位置当类类型对象。
cpp
#include<iostream>
using namespace std;
// error:运算符 '+' 必须至少包含一个类类型的参数
//int operator+(int x, int y)
//{
// return x - y;
//}
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
Date& operator++()
{
cout << "前置++" << endl;
return *this;
}
Date operator++(int)
{
Date tmp;
cout << "后置++" << endl;
return tmp;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2026, 9, 16);
Date d2(2026, 10, 1);
// 运算符重载函数可以显示调用
d1.operator==(d2);
// 编译器会转换成 d1.operator==(d2)
d1 == d2;
// 编译器会转换成 d1.operator++()
++d1;
// 编译器会转换成 d2.operator++(0)
d1++;
return 0;
}
8.2 赋值运算符重载
赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象。
赋值运算符重载的特点:
1. 赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成 const 当前类类型引用,否则会传值传参会有拷贝。
2. 有返回值,建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。
3. 没有显示实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员会完成值拷贝 / 浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用它的赋值重载函数。
4. 像 Date 这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显示实现赋值运算符重载。像 Stack 这样的类,虽然也都是内置类型,但是指向了资源,编译器自动生成的赋值运算符重载完成的值拷贝 / 浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像 MyQueue 这样的类型内部主要是自定义类型 Stack 成员,编译器自动生成的赋值运算符重载会调用 Stack 的赋值运算符重载,也不需要我们显示实现 MyQueue 的赋值运算符。这里还有一个技巧,如果一个类显示实现了析构并释放资源,那么就需要显示写赋值运算符重载,否则就不需要。
8.3 日期类实现
cpp
#pragma once
// Date class implementation
#include<iostream>
#include<assert.h>
using namespace std;
class Date
{
// friend functions
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1);
void Print()const;
int GetMonthDay(int year, int month)
{
assert(month > 0 && month < 13);
static int monthDayArray[13] = { -1, 31, 28, 31, 30, 31, 30,
31, 31, 30, 31, 30, 31 };
if (month == 2 && ((year % 4 == 0 && year % 100 != 0)
|| year % 400 == 0))
{
return 29;
}
else
{
return monthDayArray[month];
}
}
bool CheckDate();
bool operator<(const Date& d)const;
bool operator<=(const Date& d)const;
bool operator>(const Date& d)const;
bool operator>=(const Date& d)const;
bool operator==(const Date& d)const;
bool operator!=(const Date& d)const;
// d1 += value
Date& operator+=(int day);
Date operator+(int day)const;
// d1 -= value
Date& operator-=(int day);
Date operator-(int day)const;
// d1-d2
int operator-(const Date& d)const;
// ++d1 -> d1.operator++()
Date& operator++();
// d1++ -> d1.operator++(0)
Date operator++(int);
// --d1
Date& operator--();
// d1--
Date operator--(int);
private:
int _year;
int _month;
int _day;
};
// function declaration
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);
cpp
#include"test.h"
bool Date::CheckDate()
{
if (_month < 1 || _month>12
|| _day<1 || _day>GetMonthDay(_year, _month))
{
return false;
}
else
{
return true;
}
}
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
if (!CheckDate())
cout << "日期非法" << endl;
}
void Date::Print()const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
// d1 < d2
bool Date::operator<(const Date& d)const
{
if (_year < d._year)
{
return true;
}
else if (_year == d._year)
{
if (_month < d._month)
{
return true;
}
else if (_month == d._month)
{
return _day < d._day;
}
}
return false;
}
// d1 <= d2
bool Date::operator<=(const Date& d)const
{
return *this < d || *this == d;
}
bool Date::operator>(const Date& d)const
{
return !(*this <= d);
}
bool Date::operator>=(const Date& d)const
{
return !(*this < d);
}
bool Date::operator==(const Date& d)const
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
bool Date::operator!=(const Date& d)const
{
return !(*this == d);
}
Date& Date::operator+=(int day)
{
if (day < 0)
{
return *this -= -day;
}
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
Date Date::operator+(int day)const
{
Date tmp = *this;
tmp += day;
return tmp;
}
Date& Date::operator-=(int day)
{
if (day<0)
{
return *this += -day;
}
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
_month = 12;
_year--;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator-(int day)const
{
Date tmp = *this;
tmp -= day;
return tmp;
}
// d1 - d2, return days between two dates
int Date::operator-(const Date& d)const
{
int flag = 1;
Date max = *this;
Date min = d;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int days = 0;
while (min < max)
{
++days;
min += 1;
}
return days * flag;
}
// ++d1 -> d1.operator++(), pre-increment
Date& Date::operator++()
{
*this += 1;
return *this;
}
// d1++ -> d1.operator++(0), post-increment
Date Date::operator++(int)
{
Date tmp = *this;
*this += 1;
return tmp;
}
// --d1
Date& Date::operator--()
{
*this -= 1;
return *this;
}
// d1--
Date Date::operator--(int)
{
Date tmp = *this;
*this -= 1;
return tmp;
}
// output stream
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "-" << d._month << "-" << d._day << endl;
return out;
}
// input stream
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
if (!d.CheckDate())
{
cout << "日期非法" << endl;
}
return in;
}
9. 取地址运算符重载
9.1 const 成员函数
^ 把 const 修饰的成员函数称之为 const 成员函数,const 修饰成员函数放到成员函数参数列表的后面。
^ cosnt 实际修饰该成员函数隐含的 this 指针,表明在该成员函数中不能对类的任何成员进行修改。
^ 很多人误认为 const 修饰函数本身,其实不然。C++类的成员函数,底层都会隐藏一个默认参数 this 指针,用来指向当前调用函数的对象。普通成员函数的 this 指针:Student* const this (指针地址不可改,指向的内容可改);const 成员函数的 this 指针:const Student* const this (指针地址,指向内容都不可修改)。这就是 const 成员函数无法修改成员变量的本质原因:this 指针指向的对象被 const 修饰,变成只读状态。
cpp
class 类名 {
public:
// const成员函数声明
返回值类型 函数名(参数列表) const;
};
// const成员函数类外实现
返回值类型 类名::函数名(参数列表) const {
// 函数体
}
9.2 取地址运算符重载
^ 取地址运算符只能以类成员函数形式重载,不支持全局函数重载,无参数,返回值为当前类指针类型。
cpp
class 类名 {
public:
// 重载普通取地址运算符 &
类名* operator&() {
// 自定义返回地址逻辑
return this;
}
};
^ 和 const 成员函数特性一致,取地址运算符也可以重载 const 版本,适配 const 对象取地址场景,构成函数重载
cpp
class 类名 {
public:
// 非const对象取地址
类名* operator&() {
return this;
}
// const对象取地址
const 类名* operator&() const {
return this;
}
};
10. 类型转换
^ C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数
^ 构造函数前面加 explicit 就不再支持隐式类型转换
^ 类类型的对象之间也可以隐式转换,需要相应的构造函数支持
cpp
#include<iostream>
using namespace std;
class A
{
public:
// 构造函数explicit就不再⽀持隐式类型转换
// explicit A(int a1)
A(int a1)
:_a1(a1)
{
}
//explicit A(int a1, int a2)
A(int a1, int a2)
:_a1(a1)
, _a2(a2)
{
}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
int Get() const
{
return _a1 + _a2;
}
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
public:
B(const A& a)
:_b(a.Get())
{
}
private:
int _b = 0;
};
int main()
{
// 1构造⼀个A的临时对象,再⽤这个临时对象拷⻉构造aa3
// 编译器遇到连续构造+拷⻉构造->优化为直接构造
A aa1 = 1;
aa1.Print();
const A& aa2 = 1;
// C++11之后才⽀持多参数转化
A aa3 = { 2,2 };
// aa3隐式类型转换为b对象
// 原理跟上⾯类似
B b = aa3;
const B& rb = aa3;
return 0;
}
11. static 成员
^ 用 static 修饰的成员变量,称之为静态成员变量,静态成员变量一定要在类外进行初始化。
^ 静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
^ 用 static 修饰的成员函数,称之为静态成员函数,静态成员函数没有 this 指针。
^ 静态成员函数可以访问其他的静态成员,但是不能访问非静态的,因为没有 this 指针。
^ 非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
^ 突破类域就可以访问静态成员,可以通过 类名::静态成员 或者 对象**.**静态成员 来访问静态成员变量和静态成员函数。
^ 静态成员也是类的成员,受 public,protected,private 访问限定符的限制。
^ 静态成员变量不能在声明位置给缺省值初始化,因为静态成员变量不属于对象,不走构造函数初始化列表;而类内给成员写的缺省值,本质是给构造初始化列表用的。
12. 友元
^ 友元提供了一种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或者类声明的前面加 friend,并且把友元声明放在一个类的里面。
^ 外部友元函数可访问类的私有和保护成员,友元函数仅仅是一种声明,它不是类的成员函数。
^ 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
^ 一个函数可以是多个类的友元函数。
^ 友元类中的成员函数都可以是另一个类的友元函数,都可以访问另一个类的私有和保护成员。
^ 友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元。
^ 友元类的关系不能传递,如果A是B的友元,B是C的友元,但是A不是C的友元。
^ 友元有时提供了便利,但是友元会增加耦合度,破坏了封装,所以不易多用。
cpp
#include<iostream>
using namespace std;
// 前置声明,都则A的友元函数声明编译器不认识B
class B;
class A
{
// 友元声明
friend void func(const A& aa, const B& bb);
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
// 友元声明
friend void func(const A& aa, const B& bb);
private:
int _b1 = 3;
int _b2 = 4;
};
void func(const A& aa, const B& bb)
{
cout << aa._a1 << endl;
cout << bb._b1 << endl;
}
cpp
#include<iostream>
using namespace std;
class A
{
// 友元声明
friend class B;
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
public:
void func1(const A& aa)
{
cout << aa._a1 << endl;
cout << _b1 << endl;
}
void func2(const A& aa)
{
cout << aa._a2 << endl;
cout << _b2 << endl;
}
private:
int _b1 = 3;
int _b2 = 4;
};
13. 内部类
^ 如果一个类定义在另一个类的内部,这就叫内部类。内部类是一个独立的类,跟定义在全局相比,它只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。'
^ 内部类默认是外部类的友元类。
^ 内部类本质也是一种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考虑把A类设计为B的内部类,如果放到 private / protected 位置,那么A类就是B类的专属内部类,其他地方都用不了。
cpp
#include<iostream>
using namespace std;
class A
{
private:
static int _k;
int _h = 1;
public:
class B // B默认就是A的友元
{
public:
void foo(const A& a)
{
cout << _k << endl; //OK
cout << a._h << endl; //OK
}
int _b1;
};
};
14. 匿名对象
^ 用 类型(实参)定义出来的对象叫做匿名对象,相比之前我们定义的 类型 对象名(实参) 定义出来的叫有名对象。
^ 匿名对象生命周期只在当前一行,一般临时定义一个对象当前用一下即可,就可以定义匿名对象。
cpp
#include <iostream>
using namespace std;
class Person{
public:
Person(){cout<<"构造"<<endl;}
~Person(){cout<<"析构"<<endl;}
void say(){cout<<"hi"<<endl;}
};
int main(){
Person(); // 匿名对象,本行结束立刻析构
Person().say();
return 0;
}
7. 对象拷贝时的编译器优化
^ 现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少一些传参和传返回值的过程中可以省略的拷贝。
^ 任何优化C++标准并没有严格规定,个编译器会根据情况自行处理。当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化,有些更新更"激进"的编译器还会进行跨行跨表达式的合并优化。
cpp
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 0)
:_a1(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a1(aa._a1)
{
cout << "A(const A& aa)" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a1 = aa._a1;
}
return *this;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a1 = 1;
};
void f1(A aa)
{
}
A f2()
{
A aa;
return aa;
}
int main()
{
// 传值传参
// 构造+拷⻉构造
A aa1;
f1(aa1);
cout << endl;
// 隐式类型,连续构造+拷⻉构造->优化为直接构造
f1(1);
// ⼀个表达式中,连续构造+拷⻉构造->优化为⼀个构造
f1(A(2));
cout << endl;
cout << "***********************************************" << endl;
// 传值返回
// 不优化的情况下传值返回,编译器会⽣成⼀个拷⻉返回对象的临时对象作为函数调⽤表达
//式的返回值
// ⽆优化 (vs2019 debug)
// ⼀些编译器会优化得更厉害,将构造的局部对象和拷⻉构造的临时对象优化为直接构造
//(vs2022 debug)
f2();
cout << endl;
// 返回时⼀个表达式中,连续拷⻉构造+拷⻉构造->优化⼀个拷⻉构造 (vs2019 debug)
// ⼀些编译器会优化得更厉害,进⾏跨⾏合并优化,将构造的局部对象aa和拷⻉的临时对象
//和接收返回值对象aa2优化为⼀个直接构造。(vs2022 debug)
A aa2 = f2();
cout << endl;
// ⼀个表达式中,开始构造,中间拷⻉构造+赋值重载->⽆法优化(vs2019 debug)
// ⼀些编译器会优化得更厉害,进⾏跨⾏合并优化,将构造的局部对象aa和拷⻉临时对象合
//并为⼀个直接构造(vs2022 debug)
aa1 = f2();
cout << endl;
return 0;
}