
目录一类的定义1类的形式2访问限定符3类域二实例化1,实例化的概念2实例化对象的大小计算三this指针四类的默认成员函数1构造函数_2,初始化列表2析构函数3拷贝构造函数五赋值运算符重载1运算符重载2赋值运算符重载六取地址运算符重载1const成员函数2,取地址运算符重载一类的定义1类的形式1class为类的关键字Stack为类的名字{}为类的主体最后不要忘记后面的分号不要省略类体中的内容称为类的成员类中的变量称之为类的属性或类的成员变量类中的函数称之为类的方法或成员函数。2为了区分成员变量和成员函数的参数一般会在成员变量的前面加上特殊符号一般可以在前面加上_加什么符号这个C中不是强制的也没有进行规定。3定义在类中的成员函数默认为inline内联函数声明在类当中但是定义在类外面的函数就不是内联函数。//日期类 class Date { public: //成员函数 //类中的函数可以访问private中的成员变量 void Init(int year, int month, int day) { _year year; _month month; _day day; } private: //成员变量 //一般为了区分变量成员和函数的参数 //会在变量成员前面加上_或m int _year; int _month; int _day; }; int main() { Date da; da.Init(2026, 5, 23); return 0; }4C也兼容结构体的使用同时struct在C中进行了升级可以直接使用struct定义类最大的区别就是struct中也可以定义函数了不过一般还是使用class进行类的定义。//C中兼容struct的用法 typedef struct ListNodeC { int val; struct ListNodeC* next; }ListNodeC; //在C中struct升级成了类,不用在使用typedef struct ListNodeCPP { public: void Init(int x) { val x; next nullptr; } private: int val; struct ListNodeCPP* next; }; // int main() { //不用进行取别名直接使用 ListNodeCPP node; node.Init(5); return 0; }2访问限定符访问限定符是C中的一种关键字用来控制类方法属性等成员被其他地方访问的权限和范围。主要功能是实现封装隐藏内部的具体实现只暴露必要的接口保护数据的安全。访问限定符分为三种访问限定符的特性1public修饰的成员在类为能够直接被访问而private和protected修饰的成员不能被直接访问。2访问权限符的作用域是从这个访问权限符开始一直到下一个访问权限符开始结束如果下面没有访问权限符就到类的结尾结束。3class定义的成员如果没有被主动修饰则默认为被private修饰struct定义的类则默认为public.4一般的成员变量都会被修饰为private成员函数才会被修饰为public供外部进行使用。3类域1类定义了⼀个新的作用域类的所有成员都在类的作用域中在类体外定义成员时需要使用:: 作用域操作符指明成员属于哪个类域。2类域影响的是编译的查找规则下⾯程序中Init如果不指定类域Stack那么编译器就把Init当成全局函数那么编译时找不到array等成员的声明/定义在哪⾥就会报错。指定类域Stack就是知道Init是成员函数当前域找不到的array等成员就会到类域中去查找。class Stack1 { public: // 成员函数 void Init(int n 4); private: // 成员变量 int* array; size_t capacity; size_t top; }; // 声明和定义分离需要指定类域 void Stack1::Init(int n) { array (int*)malloc(sizeof(int) * n); if (nullptr array) { perror(malloc申请空间失败); return; } capacity n; top 0; } int main() { Stack1 st; st.Init(); return 0; }二实例化1,实例化的概念类是实例化的一种抽象描述限定了类有哪些成员这些成员只是进行了声明没有进行实例化没有开辟真正的内存空间。只有用类实例化出对象时才真正开辟了空间。举个例子类就相当于是一个设计图纸这个设计图纸中没有实际的空间这个设计图纸只提供这个建筑物的具体的实现细节。需要真正根据图纸进行建筑物的创建。一个类可以创建出多个实例化对象。就好比一个设计图纸可以创造出多种结构相似但是参数细节不同的建筑物。//日期类 class Date { public: //成员函数 //类中的函数可以访问private中的成员变量 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; Date D2; D1.Init(2026, 5, 20); D2.Init(2026, 5, 21); D1.Print(); D2.Print(); return 0; }2实例化对象的大小计算实例化对象大小只和类中的成员变量有关和类当中的成员函数无关。实例化对象的大小和结构体的大小计算类似也要遵从内存对齐的规则。内存对齐的规则1第一个成员在偏移量为0的地址处。2其他的成员需要对齐到对齐数的整数倍处。对齐数编译器的默认对齐数和该成员变量的大小的较小值。VS编译器的默认对齐数为83结构体的大小必须为最大对齐数的整数倍。最大对齐数 所有变量成员对齐数的最大值。4如果嵌套了结构体的情况嵌套的结构体对齐到自己的最大对齐数的整数倍处结构体的整体大小就是所有最大对齐数含嵌套结构体的对齐数的整数倍。类A的计算上面的类B,类C必须给一个字节否则无法证明对象存在如果连内存空间都没有就便无法证明其存在。三this指针在C语言中创建一个成员在调用函数时往往需要传入一个该变量地址从而确定是哪个变量进行了函数调用。而在C中则不需要特别传入就可以确定是那个变量进行了调用这时为什么呢其实C在传入参数时会默认传入一个名为this指针的指针而类中的成员函数也会隐藏这个负责接收的形参这个函数的原型为void Init(Date* const this, int year, int month, int day)类中的成员函数访问成员变量本质上也是通过this指针进行访问的例如需要注意的是不能在形参和实参的位置处放入this指针这是C规定但是可以在函数体内使用this指针如上不过一般会被省略。补充this指针在x86环境下一般存放在栈当中在x64环境下一般存放在寄存器当中。四类的默认成员函数默认成员函数就是用户没有进行显示实现但是编译器会进行自动实现的成员函数称为成员们默认成员函数一个类当中会默认生成6个默认成员函数。面对这6个默认成员函数我们需要知道1我们不写时编译器默认生成的函数行为是什么是否满足我们的需求2编译器默认生成的函数不满足我们的需求我们需要自己实现那么如何自己实现1构造函数构造函数是特殊的成员函数主要的功能是对象实例化时初始化对象。构造函数的本质是要替代我们以前写的Init函数的功能构造函数自动调用的特点就完美的替代的了Init。构造函数的特点1函数名与类名相同。2无返回值。 (也不需要写void)3对象实例化时系统会自动调用对应的构造函数。4构造函数可以重载。5如果类中没有显式定义构造函数则C编译器会自动生成⼀个无参的默认构造函数⼀旦用户自己显式定义编译器将不再生成。6⽆参构造函数、全缺省构造函数、不写构造时编译器默认⽣成的构造函数都叫做默认构造函 数。但是这三个函数有且只有⼀个存在不能同时存在。⽆参构造函数和全缺省构造函数虽然构成 函数重载但是调用时会存在歧义。全缺省构造函数在声明时需要全缺省在定义时不需要全缺省。易错点认为默认构造函数是编译器默认生成那个叫 默认构造实际上无参构造函数、全缺省构造函数也是默认构造总结⼀下就是不传实参就可以调用的构造就叫默认构造。class Date { public: //1,无参构造函数 Date() { _year 2026; _month 5; _day 22; } //2带参的构造函数 Date(int year, int month, int day) { _year year; _month month; _day day; } //3,全缺省的构造函数 //Date(int year 2026, int month 5, int day 23) //{ // _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, 8, 4);//调用带参 //通过无参构造函数进行实例化对象后面不用跟括号 //否则编译器无法区分这里是函数声明还是函数的实例化 //Date d3(); d1.Print(); d2.Print(); return 0; }7我们不写构造函数使用编译器默认生成的构造函数对内置类型成员变量的初始化是不确定的由编译器决定。对于自定义类型成员变量要求调用自定义类型中的成员变量的默认构造函数已经初始化。如果这个成员变量没有默认构造函数那么就会报错我们要初始化这个成员变量需要用初始化列表才能解决。说明C把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的原生数据类型如int/char/double/指针等⾃定义类型就是我们使用class/struct等关键字自己定义的类型。实例展示typedef int STDataType; class Stack { public: Stack(int n 4) //(int n) { a (STDataType*)malloc(sizeof(STDataType) * n); if (nullptr _a) { perror(malloc申请空间失败); return; } _capacity n; _top 0; } // ... private: STDataType * _a; size_t _capacity; size_t _top; }; // 两个Stack实现队列 class MyQueue { public: //编译器默认⽣成MyQueue的构造函数调⽤了Stack的构造完成了两个成员的初始化 private: Stack pushst; Stack popst; }; int main() { MyQueue mq; return 0; }这里使用自定义类型class,class中的成员变量pushst,popst都在Stack中有默认构造函数Stack(全缺省构造函数)如果将int n 4 改成 int n 就会报错因为没有默认构造函数此时需要初始化列表才能解决。_2,初始化列表概念理解之前使用构造函数时进行成员变量的初始化主要是在函数体内进行赋值构造函数的还有一种初始化成员变量的方法就是初始化列表。初始化列表和函数体可以进行混用初始化列表的形式是从函数名的下一行开始以冒号开始初始化第一个变量随后到下一行以逗号隔开写出成员名以及括号中的初始化值。展示初始化列表的特点_1,初始化列表是成员初始化的地方所以成员变量只能在初始化列表中出现一次_2,三种变量只能放在初始化列表当中进行初始化分别是引用成员变量const成员变量无默认构造函数的类类型变量。原因引用成员变量和const成员变量需要直接初始化否则使用默认值会后续无法进行更改对于无默认构造函数的类类型变量就需要自己进行传参调用所以需要进行初始化演示class Time { public: //若为全缺省构造函数--默认构造函数 Time(int hour 1) :_hour(hour) { cout Time() endl; } private: int _hour; }; class Date { public: //全缺省构造函数--这个缺省值可以在不手动传参数时直接完成初始化 Date(int s,int year 8, int month 8, int day 8) :_year(year) //成员变量初始化内容 ,_month(month) ,_day(day) ,_ref(s) ,_n(7) //,ti(4) //此时不强制进行手动初始化当然也可以手动传值 //函数体 { } void Print()const { cout _year - _month - _day endl; } private: int _year; int _month; int _day; int _ref; const int _n; Time ti; }; int main() { int k 3; Date d(k); d.Print(); return 0; }_3,不写在初始化列表中的成员变量也会调用初始化列表如果这个成员给了缺省值两种方式就会使用缺省值如果没给缺省值也不手动传入参数对于内置类型是否进行初始化由编译器决定即使编译器进行初始化也不会报错对于自定义类型的成员会调用默认构造函数如果没有就会报错。class Time { public: Time(int hour) :_hour(hour) { cout Time() endl; } private: int _hour; }; class Date { public: //全缺省构造函数--这个缺省值可以在不手动传参数时直接完成初始化 Date() //即使不写也会调用初始化列表此时使用的是缺省值 :_month(4) //手动传参 {} void Print()const { cout _year - _month - _day endl; } private: //这不是初始化这是给初始化列表的缺省值没有写就用缺省值 int _year 1; int _month 3; //内置类型如果在初始化列表中不手动传值也不传入缺省值 int _day; //是否初始化就取决于编译器 int* ret (int*)malloc(3); const int _n 3; //如果构造函数不是默认构造函数同时也没有传入缺省值就会报错 Time ti 3; }; int main() { Date d; d.Print(); return 0; }_4,初始化列表中的成员按照声明时的先后顺序进行初始化而不是按照初始化列表当中的顺序进行初始化当然两者的顺序建议一致。初始化列表总结1无论是否显示写初始化列表每个构造函数都有初始化列表。2无论是否在初始化列表当中写初始化成员每个成员变量都会走初始化列表。2析构函数析构函数的功能和构造函数的功能相反他不是完成对对象本身的销毁对象的销毁会在函数调用完函数栈帧自动进行释放。析构函数真正的功能是完成对资源的清理释放工作。可以类比在C语言中栈数据结构的实现中的Destory功能。析构函数的特征1析构函数名前需要加上~2析构函数无返回值无参数。也不需要加上void3一个类当中只有一个析构函数当用户未定义时会使用编译器生成的析构函数。4对象的生命周期结束会自动调用析构函数。5同构造函数一样当用户不写析构函数时调用编译器自动生成的析构函数时对内置类型对象不做处理调用栈帧后自然会销毁对自定义类型会调用他的析构函数。6自定义类型的对象无论什么情况都会调用他的析构函数不论用户是否指定析构函数。7,一个局部域中有多个对象时后定义的对象会先析构。#includeiostream using namespace std; class Stack { public: //构造函数 Stack(int n 4) { _a (int*)malloc(sizeof(int) * n); if (nullptr _a) { perror(栈空间开辟失败); return; } _capacity n; _top 0; } //析构函数 ~Stack() { free(_a); _a nullptr; _capacity _top 0; } private: int* _a; size_t _capacity; size_t _top; }; //两个stack实现队列 class Myqueue { public: //使用编译器默认生成的析构函数 ~Myqueue() { cout Myqueue() endl; } private: //自定义类型调用本身的析构函数 Stack pushst; Stack popst;//后定义的变量先析构 }; int main() { //Stack st; Myqueue qu; return 0; }3拷贝构造函数定义如果一个构造函数的第一个参数是自身类类型的引用其他的形式参数都有默认值那么这样的构造函数称为拷贝构造函数拷贝构造也是构造函数的一种以拷贝的形式进行构造。拷贝构造函数的特点1拷贝构造函数是构造函数的一个重载。2拷贝构造函数的第一个参数必须是类类型的引用如果调用拷贝构造函数第一个参数值使用的是传值调用那么就会出现无穷递归的情况。拷贝构造函数可以同时传入多个参数除去第一个外构造函数后面的形参参数默认有缺省值。无穷递归3C中自定义对象进行拷贝行为时必须先调用拷贝构造进行创建自定义类型的参数使用传值调用和传值返回都会调用拷贝构造函数。d就是d1的引用两者拥有同样的空间 但是创建的this指针指向的对象就相当于d的拷贝 调用后创建完成就直接返回调用Func1函数。先调用拷贝构造生成拷贝值再调用函数这个拷贝值*this有独立的空间。4若未显示拷贝构造会使用编译器生成的拷贝构造编译器自动生成的拷贝构造会对内置类型会进行浅拷贝/值拷贝一个字节一个字节的拷贝对自定义类型的成员会调用他的拷贝构造。5当创建的类中只有内置类型此时使用编译器定义的拷贝构造函数即可当遇到Stack这样的类时类中有资源的开辟此时使用值拷贝就不能完成目标就需要自己创建显式的拷贝构造函数。当类当中只有自定义类型时此时也不需要进行显式的拷贝构造函数自定义类型会自己调用他的拷贝构造函数。判断技巧如果一个类实现了析构并释放了资源那么就需要进行拷贝构造函数的创建否则就不需要。如果不重新开辟资源直接使用浅拷贝则两次指向的空间一致会对同一块空间造成析构两次导致程序崩溃。#include iostream using namespace std; class Date { public: //构造函数 Date(int year 1, int month 1, int day 1) { x year; y month; z day; } //Date(const Date st) //{ // x st.x; // y st.y; // z st.z; //} Date(Date* st) //普通构造 { x st-x; y st-y; z st-z; } void Print(Date d) { cout d x y z endl; } private: int x; int y; int z; }; //栈 class Stack { public: //构造函数 Stack(int n 4) { _a (int*)malloc(sizeof(int) * n); if (nullptr _a) { perror(malloc申请空间失败); return; } _capacity n; _top 0; } //使用拷贝构造函数 Stack(const Stack st) { //开辟一个和原空间相同大小的新空间 _a (int*)malloc(sizeof(int) * st._capacity); if (nullptr _a) { perror(新空间开辟失败); return; } memcpy(_a, st._a, sizeof(int)* st._top); _capacity st._capacity; _top st._top; } void Push(int x) { if (_top _capacity)//证明空间已满 { //扩大空间为原来的两倍 int newcapacity _capacity * 2; //再次创建一个空间而不是直接使用_a是为了防止返回realloc失败返回NULL,将_a进行覆盖 int* tmp (int*)realloc(_a,sizeof(int) * newcapacity); if (nullptr tmp) { perror(空间开辟失败); return; } _a tmp; _capacity newcapacity; } //_top指向的是栈顶元素的下一个的空空间 _a[_top] x; } //析构函数 ~Stack() { free(_a); _a nullptr; _top _capacity 0; } private: int* _a; int _capacity; int _top; }; //两个栈实现队列 class MyQueue { public: private: Stack pushst; Stack popst; }; int main() { Date d1(2024,7,22); //使用编译器的拷贝构造函数--浅拷贝 //传入的d1是引用和d2不是一个类型不是拷贝构造只是一个普通构造 Date d2(d1); Date d3(d1); //这样才是拷贝构造 Date d4 d3; //另一种拷贝构造的写法 d1.Print(d1); d3.Print(d3); d1.Print(d4); Stack st1; st1.Push(1); st1.Push(2); //有资源开辟的类成员的构造--使用深拷贝 Stack st2(st1); MyQueue qu1; //类成员都是自定义类型--调用成员的拷贝构造 MyQueue qu2(qu1); return 0; }6传值返回会使用一个临时对象进行接收相当于开辟了同返回值大小的新空间而传引用返回不会开辟新的空间而是一个引用节省了空间但是如果返回的值是函数的一个局部变量。那么这个变量在出函数就会销毁销毁的变量就会成为一个野指针所以在使用传值返回时要考虑返回值是否是函数创建的局部变量。五赋值运算符重载1运算符重载运算符重载的特点1当运算符被使用在类类型对象时C规定必须使用运算符重载指定新的含义进行操作反而当没有运算符重载时直接进行运算时编译会报错。2运算符重载的名字比较特殊由operator和后面使用的运算符一同构成运算符重载和函数类似有函数体和参数返回值类型类似函数。3重载运算符的参数个数和函数名后面的操作符的操作对象的个数相同。二元操作符就有两个参数三元操作符就有三个参数。当重载运算符函数是一个类类型成员函数时他的第一个参数会默认传给this指针因此实际的参数会比操作符的操作数要少一个。4运算符重载之后的优先性和结合性不改变。5_1,不能使用没有运算含义的符号定义运算符重载函数例如 operator# _2,不能改变原来的运算符含义例如 在operator() 中定义-操作 _3,定义的运算符重载函数一定要有实际的含义。例如在Date类当中定义operator- 可以计算两个日期之间的差值但是定义operator计算日期之和就没有实际意义_4,运算符重载函数的参数中至少有一个是类类型参数不能都是内置类型 。例如operatorint x ,int y (X)错误6定义的运算符重载时有前置和后置,都是operator为了方便区分他们C规定在使用后置进行重载时需要加上一个int类型的形参跟前置进行区分。7在C中有五个运算符不能进行重载分别是1作用域解析运算符 2成员访问运算符 . 3条件运算符 4sizeof运算符 sizeof() 5成员指针访问运算符 .*补充关于成员指针访问运算符成员函数指针类型2赋值运算符重载赋值运算符函数是一个 默认成员函数用于完成两个已经存在的对象的直接赋值拷贝赋值注意和拷贝构造进行区分拷贝构造是将一个已经存在的函数初始化拷贝给另一个将要创建的对象。赋值运算符重载的特点1赋值运算符重载是一个运算符重载规定必须重载为成员函数。赋值运算符重载的参数建议写成const 类类型的引用使用传值传参会产生拷贝。2返回值建议写成类类型的引用提高效率的同时满足连续赋值的场景。3没有显示实现赋值运算符重载时系统会使用编译器默认生成的赋值运算符重载。和拷贝构造类似对于系统的内置类型会完成值拷贝/浅拷贝一个一个字节的拷贝。对于自定义类型来说会调用他的赋值重载函数。4同拷贝构造一样当创建的类中只有内置类型此时使用编译器定义的赋值运算符函数即可当遇到Stack这样的类时类中有资源的开辟此时使用值拷贝就不能完成目标就需要自己创建显式的赋值运算符函数。当类当中只有自定义类型时此时也不需要进行显式的赋值运算符函数自定义类型会自己调用他的赋值运算符函数。判断技巧如果一个类实现了析构并释放了资源那么就需要进行赋值运算符函数的创建否则就不需要。class Date { public: //构造函数 Date(int year 1, int month 1, int day 1) { _year year; _month month; _day day; } //拷贝构造函数 Date(const Date d1) { _year d1._year; _month d1._month; _day d1._day; } //d1 d2 返回d1 Date operator(const Date d2) { _year d2._year; _month d2._month; _day d2._day; return *this; } void Print() { cout _year _month _day endl; } //在类当中定义的成员函数第一个参数进行隐藏--this指针 private: int _year; int _month; int _day; }; int main() { Date d1(2024,5,31); //拷贝构造 Date d2(d1); Date d3(2002, 4, 5); d1.Print(); //赋值运算符重载 d1 d2 d3; d1.Print(); d2.Print(); d3.Print(); return 0; }六取地址运算符重载1const成员函数const成员函数的const放在成员函数的参数列表后面。const实际修饰的是函数成员隐含的this指针表明不能对类当中的任何成员进行修改。举例const修饰Date类的成员函数Print隐含的this指针由Date* const this 变成 const Date* const thisvoid Date::Print() const //const Date* const this { //前一个const表示this指向的成员不能变而后一个this表示this指向的地址不能变 cout _year _month _day endl; }2,取地址运算符重载取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载一般这两个函数可以由编译器自动生成而编译器自动生成的就足够满足我们日常的使用。//普通对象取地址符重载 Date* operator() { return this; } //const取地址符重载 const Date * operator()const { return this; }