目录
一,类的定义
1,类的形式
2,访问限定符
3,类域
二,实例化
1,实例化的概念
2,实例化对象的大小计算
三,this指针
四,类的默认成员函数
1,构造函数
_2,初始化列表
2,析构函数
3,拷贝构造函数
五,赋值运算符重载
1,运算符重载
2,赋值运算符重载
六,取地址运算符重载
1,const成员函数
2,取地址运算符重载
一,类的定义
1,类的形式
1,class为类的关键字,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; }4,C++也兼容结构体的使用,同时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++中的一种关键字,用来控制类,方法,属性等成员被其他地方访问的权限和范围。主要功能是实现封装,隐藏内部的具体实现,只暴露必要的接口,保护数据的安全。
访问限定符分为三种:
访问限定符的特性:
1,public修饰的成员在类为能够直接被访问,而private和protected修饰的成员不能被直接访问。
2,访问权限符的作用域是从这个访问权限符开始一直到下一个访问权限符开始结束,如果下面没有访问权限符就到类的结尾结束。
3,class定义的成员如果没有被主动修饰则默认为被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编译器的默认对齐数为8
3,结构体的大小必须为最大对齐数的整数倍。
最大对齐数 = 所有变量成员对齐数的最大值。
4,如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
上面的类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,构造函数
构造函数是特殊的成员函数,主要的功能是对象实例化时初始化对象。构造函数的本质是要替代我们以前写的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,析构函数无返回值,无参数。(也不需要加上void)
3,一个类当中只有一个析构函数,当用户未定义时,会使用编译器生成的析构函数。
4,对象的生命周期结束,会自动调用析构函数。
5,同构造函数一样,当用户不写析构函数时,调用编译器自动生成的析构函数时,对内置类型对象不做处理,调用栈帧后自然会销毁;对自定义类型,会调用他的析构函数。
6,自定义类型的对象无论什么情况都会调用他的析构函数,不论用户是否指定析构函数。
7,一个局部域中有多个对象时,后定义的对象会先析构。
#include<iostream> 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,拷贝构造函数的第一个参数必须是类类型的引用,如果调用拷贝构造函数第一个参数值使用的是传值调用,那么就会出现无穷递归的情况。拷贝构造函数可以同时传入多个参数,除去第一个外构造函数后面的形参参数默认有缺省值。
无穷递归:
3,C++中自定义对象进行拷贝行为时,必须先调用拷贝构造进行创建,自定义类型的参数使用传值调用和传值返回都会调用拷贝构造函数。
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,运算符重载函数的参数中至少有一个是类类型参数,不能都是内置类型 。例如operator+(int x ,int y) (X)错误
6,定义++的运算符重载时,有前置++和后置++,都是operator++,为了方便区分他们,C++规定,在使用后置++进行重载时,需要加上一个int类型的形参,跟前置++进行区分。
7,在C++中,有五个运算符不能进行重载,分别是:1,作用域解析运算符:: 2,成员访问运算符 . 3,条件运算符 ?:4,sizeof运算符 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; }六,取地址运算符重载
1,const成员函数
const成员函数的const放在成员函数的参数列表后面。const实际修饰的是函数成员隐含的this指针,表明不能对类当中的任何成员进行修改。
举例:const修饰Date类的成员函数,Print隐含的this指针由Date* const this 变成 const Date* const this
void 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; }