类
类的含义理解
首先梳理一下类和明明空间两者的关系,例如将自己购物拆分成的几个过程,需要主体人、支付手机、超市,三种主体配合才可以完成购物。这三者形成的购物过程,可以称为命名空间,而人、手机、超市可以理解成类,因为它们只可以完成某一种功能。
其次理解成员函数和成员变量,定义一个类后,将这个类还当做上述的超市,那么只有超市中的机器实现的功能叫做成员函数,超市中的物品则称为这个类成员变量。
class shop
{
.......
};
class是定义类的关键字,show类的名字,{}中是类的主体,其中包括类的成员(成员函数和成员变量)。
访问限定符
类中的访问限定符主要有三个
- public:类内类外都可以直接访问
- protected、private:类外不能够直接访问
访问权限的作用域:从该访问限定符开始的位置到下一个访问限定符出现的位置。如果后续没有访问限定符,则到最后的‘}’结束。
类class的默认访问限定符是private,struct的默认限定符是public。访问限定符只有在编译的时候有用,当数据映射到内存后,数据便没有访问限定符的限制。
C++中的struct和clss区别和联系:因为C++需要兼容C语言,struct就可以当做结构体来用,又可以当做的class来用,定义和使用方式和类相同。两者又有不同之处,class的默认访问权限是private而struct的默认访问权限是public。
封装
C++的封装即是将数据和函数的实现都封装在一个类中,隐藏函数的具体实现的细节,只通过对外公开的接口和对象进行交互。封装的本质也是一种对数据和函数的管理,让用户可以更加方便的使用类。可以理解成电脑通过外壳将里面器件进行封装,只是暴露出接口供用户使用。
C++中实现封装的具体方法,通过类将操作数据和操作数据的方法有机结合,通过访问权限去隐藏对象内部的具体实现细节。控制哪些方法可以使用,哪些方法需要进行隐藏。
封装的优点
确保用户代码不会无意间破坏封装对象的状态
被封装的类的具体细节可以随时改变,而不需要调整用户级别代码
类的作用域
类定义一个新的作用域,类的所有成员都在类的作用域中。在类外定义成员的时候,需要 :: 声明作用域操作符指明成员属于哪个类域。
class student
{
public:
void PrintStudent();
private:
int _num;
char _name[20];
};
void student::PrintStudent()
{
cout << _num << ":" << _name << endl;
}
类的实例化
类类型创建对象的过程便是类的实例化。例如上述代码中,使用student便可以实例化一个学生类,student类似于一个学生模版,只有当学生小明真实存在时候,里面才会填充信息,相应的物理空间中只有实例化后才会分配对应的物理内存。
class student
{
public:
void PrintStudent();
int _num;
char _name[20];
};
int main()
{
student xiaoMing;
xiaoMing._num = 1010;
}
类对象大小
类在内存中存储并不是整个类函数的变量都存放在一个地方。类中的成员函数存储在公共代码区,只有类变量开辟物理内存。所以类的大小也就是这些类的成员变量大小。所以计算其大小时需要遵循内存对齐原则。
内存对齐
理解内存对齐,编译器将程序中的每个数据单元地址安排在机器字的整数倍的地址指向内存中。编译器存储数据的时候并不是按照顺序依次向内存中存入数据,因为访问特定类型的数据需要在特定的内存地址空间访问,所以需要对这些数据存储为位置进行限制,而对于这些数据的限制也就是内存对齐。
内存对齐的原因
- CPU访问内存特性决定。CPU访问内存的时候并不是以字节为单位读取内存,而是以机器字长为单位读取内存。
- 机器字长长度由CPU数据总线宽度决定。每次控制内存读写信号发生后,CPU从内存中读取数据总线宽度,然后将其写入到CPU通用寄存器中。32位的CPU,机器字长就是4个字节,数据总线宽度是32位。
- 减少CPU访问内存的次数,增加CPU访问内存吞吐量。
- 硬件原因,某些特定的硬件设备只能够存储对齐数据,存取非对齐数据会引发异常。某些处理器虽然支持访问非对齐数据,但是会引发对齐陷阱。某些硬件只支持简单的非对齐数据拿取,不支持复杂数据指令的非对齐存储。
- 性能原因:数据结构应该尽可能自然边界对齐,为了访问未对齐的内存,处理器需要进行两次内存访问,而对其只需访问内存依次即可。
内存对齐原则
- 第一个成员在与结构体变量偏移量为0地址处
- 其他成员变量要对齐到对齐数的整数倍
- 对齐数= min(编译器默认一个对齐数 , 该成员大小)
- VS中默认对齐数是8,Linux没有默认对齐数,对齐数就是成员变量自身大小
- 结构体总大小为最大对齐数(取所有成员变量中最大的对齐数)的整数倍
- 如果有嵌套结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍,结构体的整体大小就是所有最大对齐数的整数倍。
大小端
- 大端:高位字节排放在内存的低地址端,底位字节排放在内存的高地址处
- 小端:低字节放在内存低地址端,高字节放在高地址端
- 假设数据:0x44332211
补充知识(大小端和内存对齐):https://juejin.cn/post/7002577434432274440
this指针
this指针的含义和使用场景分析。类中成员函数需要给成员变量赋值时或者使用该变量时,就需要使用到this指针。C++编译器器给每个“非静态成员函数”增加了一个隐藏指针参数,让该指针指向当前对象(类外调用该函数时调用该函数的对象),在函数体中所有“成员变量”的操作都是通过this指针完成,只是编译器隐藏了这些实现。
class student
{
public:
int _num;
char _name[20];
public:
void PrintStudent(int num)
{
//this->_num = num;
_num = num;
}
};
int main()
{
student xiaoMing;
xiaoMing._num = 1010;
cout << xiaoMing._num<< std::endl;
xiaoMing.PrintStudent(9999);
cout << xiaoMing._num << std::endl;
}
this指针的特点
- this指针的类型是 类类型*const,所以成员函数中是不能够给this指针赋值
- 静态成员函数中没有this指针
- this指针只能够在成员函数的内部使用
- this本质上是成员函数的形参,当对象调用成员函数的时候,将对象地址作为实参传递给this形参,所以对象中不存储this指针
- this指针是成员函数第一个隐含的指针形参,一般情况下是编译器通过ecx寄存器自动传递,不需要用户进行传递。
tthis指针一般存储在栈帧中,但是在VS中进行了优化,而是通过寄存器exc传递this指针。
类的6个默认成员函数
默认成员函数指的是即是我们创建一个空类,类中会默认创建的函数,具体来说应该是编译器生成的成员函数。
- 初始化和清理
- 构造函数:初始化
- 析构函数:清理工作
- 拷贝赋值
- 拷贝函数:使用同类对象初始化创建对象
- 赋值重载:把一个对象赋值给另一个对象
- 取地址重载
- 普通对象和const对象取地址
构造函数
构造函数的名字与类名相同,创建类类型对象时由编译器的自动调用,保证每个成员变量有一个合适的初始值,并且在对象整个声明周期内只调用一次。构造函数的目标不是开辟空间创建对象,而是初始化对象。
构造函数无返回值,函数名和类名相同,对象实例化的时候编译器自动调用对应的构造函数,构造函数可以实现重载。
无参构造函数创建对象时,对象后不需要加(),避免变成函数声明。
class student
{
public:
int _num;
char _name[20];
public:
student()//无参构造
{}
student(int num)//带参构造
{
this->_num = num;
}
};
int main()
{
/*student xiaoLi();*///错误写法
student xiaoMing;
student xiaoHong(99);
return 0;
}
类中如果没有显式构造函数,则编译器自动调用类中默认构造函数,如果有显式构造函数则不会调用默认构造函数,而是使用显式构造函数。
C++构造函数主要初始化两种,一种是内置变量(例如Int,char),另一种是自定义类型(class、struct),编译器初始化自定义类型时,会调用该自定义类型的默认成员函数。C++11中也对内置类型不初始化打了补丁,内置类型成员变量在类中声明可以给默认值。
class book
{
public:
book()
{
//C++11改进
_num = 100;
}
private:
int _num;
};
class student
{
public:
int _num;
char _name[20];
book mag;//调用book的构造函数
public:
student()//无参构造
{}
student(int num)//带参构造
{
this->_num = num;
}
}
构造函数体中赋值和初始化
构造函数体中可以对变量实现多次赋值操作,但是类中成员变量的初始只能够进行一次。类成员的初始化是在类初始化列表中。
class book
{
public:
//book()
//{
// //C++11改进
// _num = 100;
//}
//初始化列表对成员变量进行初始化
book() :_num(199)
{}
private:
int _num;
};
必须初始化列表初始化的变量
- 引用成员变量
- const成员变量
- 自定义类型的成员
class student
{
student(book wj,int x,int y)
:_mag(wj),
_w(x),
_n(y)
{}
public:
book _mag;//调用book的构造函数
int& _w;
const int _n;
};
使用初始化列表时需要注意点,其一初始化列表的顺序必须和成员变量的顺序一致,其二优先使用初始化列表对成员变量初始化,提高访问效率。
explicit
作用理解,当传入的数据类型和类中类型不一致的时候,类内部会发生自动转换,使用该关键自字就是禁止这种隐形转换。
析构函数
析构函数负责在对象销毁时,完成对类中资源的清理工作。析构函数返回值void,在构造函数的基础上添加~符号即可。一个类只能够存在一个析构函数,析构函数是不可以重载,C++编译系统会自动调用析构函数。
如果类的成员变量中有自定义类型的成员,则会调用该自定义类型成员的析构函数,并不会在类中直接析构。
class book
{
public:
book() :_num(199)
{}
~book()
{}
private:
int _num;
};
class student
{
student(book wj,int x,int y)
:_mag(wj),
_w(x),
_n(y)
{}
~student()
{}
public:
book _mag;//调用Book中的析构函数
int& _w;
const int _n;
};
拷贝构造函数
拷贝构造函数就是用已经存在的类类型对象创建爱新的对象,可以简单的理解称为类自己创建了自己。拷贝构造函数只有单个形参,该形参是对本类类型对象的引用(一般要使用const修饰),自己创建自己的时候由编译器自动调用。
拷贝构造函数的特性
- 拷贝构造函数时构造函数的一种重载方式。构造函数是根据传入的数值或则默认赋予初始值的方法,而拷贝构造函数则是传入一个该类创建的类对象然后构造。将打印机理解成一个工具类,打印机可以默认打印出测试文档,但是通过这台打印机的复印功能也可以实现对打印文档的复印。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,如果直接使用传值方式会引起无穷递归最终导致编译器报错。
- 如果使用编译器生成的默认拷贝函数,会造成钱拷贝。因为编译器生成的拷贝函数时按照字节序直接进行拷贝的,而自定义类型是调用其拷贝构造函数的进行拷贝。
- 浅拷贝,重新在堆中创建内存,拷贝后对象的基本数据互不影响。但是不能够对对象中的内容进行拷贝。所以最终在释放该块内存时,会多次释放,从而造成程序崩溃。
- 类中如果没有涉及到资源分配时,浅拷贝不会出错,但是只要涉及到资源分配,必须在类中构建拷贝构造函数,避免浅拷贝。
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}
//运行后导致程序崩溃
- s1对象调用类的构造函数,默认申请10个空间,然后存入元素1234
- s2对象使用s1进行拷贝构造,但是该类中没有拷贝构造函数,所以此时拷贝是浅拷贝。默认默认拷贝是按照值进行拷贝,所以两个对象s1 s2在物理内存中指向同一片空间。
- s1和s2两个对象退出的时候,会调用两次的析构函数,对该块物理内存释放两次,最终导致程序崩溃。
拷贝构造函数使用的场景分析
class Date
{
public:
Date(int year, int minute, int day)
{
cout << "Date(int,int,int):" << this << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d):" << this << endl;
}
~Date()
{
cout << "~Date():" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
Date Test(Date d)
{
Date temp(d);
return temp;
}
int main()
{
Date d1(2022, 1, 13);
Test(d1);
return 0;
}
//结果
Date(int,int,int):0075FE4C
Date(const Date& d):0075FD3C
Date(const Date& d):0075FD10
Date(const Date& d):0075FD6C
~Date():0075FD10
~Date():0075FD3C
~Date():0075FD6C
~Date():0075FE4C
赋值运算复符重载
运算符重载
运算符重载是具有特殊函数名的函数,具有返回值类型、函数名字、参数列表,主要区别便是函数名部分使用operator+重载运算符实现。
函数原型:关键字 operator 操作符 (参数列表)
类成员函数在类内定义重载时,参数和类外定义少一个,因为成员函数的第一个参数为隐藏的this指针。
class Student
{
public:
Student(int id,int num )
:_id(id),_num(num)
{}
//类内重载
bool operator == (const Student& s2)
{
return _id == s2._id && _num == s2._num;
}
public:
int _id;
int _num;
};
//类外重载
bool operator==(const Student& s1, const Student& s2)
{
return s1._id == s2._id && s1._num == s2._num;
}
赋值运算符重载格式
参数类型:const T&,传递引用提高传参效率
返回类型:T&,返回引用可以提高返回效率,返回值的目的是为了支持连续赋值
检测是否自己给自己赋值;返回*this
与拷贝赋值不同点:着重关注其返回值
class Student
{
public:
Student(int id,int num )
:_id(id),_num(num)
{}
Student& operator=(const Student& st)
{
_id = st._id;
_num = st._num;
}
public:
int _id;
int _num;
};
int main()
{
Student s1(1, 22);
Student s2 = s1;
cout << s2._id << endl;
return 0;
}
赋值运算符只能够重载成类的成员函数不能重载成全局函数
编译器默认生成赋值运算符重载
编译器自动生成的赋值运算符重载是以值的方式逐字节进行拷贝。内置类型成员是可以直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。简单来说则是默认生成的赋值运算符重载同样会造成浅拷贝问题,所以涉及到内存管理的时候一定的要自己实现赋值运算符重载,不能够使用编译器自己生成的。
前置++和后置++重载
后置++和前置++的区别在于,后置++在重载的时候多增加一个int类型的参数。
class Student
{
public:
Student(int id,int num)
:_id(id),_num(num)
{}
//前置++
Student& operator++()
{
_num++;
return *this;
}
//后置++
Student operator++(int)
{
Student temp(*this);
_num++;
return temp;
}
public:
int _id;
int _num;
};
int main()
{
Student s1(1, 22);
Student s2 = s1++;
cout << "s2=" << s2._num << endl;
Student s3 = ++s2;
cout << "s3=" << s3._num << endl;
return 0;
}
static成员
static的类成员称为类的静态成员,static修饰的成员变量成为静态成员变量,static修饰的成员函数则是静态成员函数。静态成员变量是在类内定义类外初始化。
static成员的特征
- 静态成员被所有类对象共享,不属于某个具体对象,数据存放在静态区中
- 静态成员变量必须在类外定义,只需添加类中声明即可
- 类静态成员可以用 类名 :: 静态成员 和 对象.静态成员来访问
- 静态成员没有隐藏this指针,不能够访问任何非静态成员
static全局静态变量和普通全局变量
- 联系:普通全局变量和静态全局变量都是静态的存储方式;
- 作用域区别:
- 普通全局变量作用域是整个源程序,一个源程序的所有源文件都可以调用普通全局变量
- 静态全局变量只在定义该变量源文件内有效。静态全局变量只能够初始化一次,防止在其他文件中使用,但是全局静态变量存储在在内存的静态区中,生命周期贯穿整个执行流。
- 总结记忆:两者都存在静态存储区中,普通全局可以在整个源程序中起作用,静态全局只能在定义的源文件中起作用。static“封印了该变量的作用域”。
static局部静态变量
局部变量只能够被初始化一次,但是作用域的仅限于函数内部,它的作用域与函数内部的局部变量是相同的。实际上局部静态变量也是存储在静态存储区中,其生命周期也是贯穿整个程序的运行期间。
static静态成员变量
静态成员变量是在类内进行声明,类外进行初始化。类外进行初始化时,不要出现static、private、public等关键字。
- 静态成员变量相当于类域中全局变量,被类的所有对象共享,包括派生类对象。
- 静态成员变量只能够被初始化一次,只能在类外进行初始化,不能够在构造函数中对其初始化
- 静态成员变量可以做成员函数的参数,而普通成员变量不可以
- 静态成员类型可以是所属类的类型,而普通数据成员的类型只能是该类类型的指针和引用
class mag
{
private:
static int _num;
int _id;
public:
//正确
void f1(int i = _num)
{
}
//错误:不可以用普通成员变量
void f2(int j = _id)
{
}
};
int mag::_num = 100;
class mag
{
private:
static mag gg;
mag aa;//错误
mag* p;
mag& ptr;
static int _num;
int _id;
public:
}
static静态成员函数
- 静态成员函数不能够调用非静态成员变量或者非静态成员函数,因为静态成员函数没有this指针。静态成员函数作为类作用域的全局函数。
- 静态成员函数不能声明成虚函数、const函数、volatile函数
友元
友元提供了一种突破封装的方,只要A类标记自己是B类朋友,则B类就以调用A类的成员变量。但是需要注意的是如果B类没有说自己是A的朋友时,A是不可以调用B类资源。可以简单理解为A认为B是自己朋友,将自己的资源与B共享,但是B并不把A当做朋友,不会和A共享自己的资源。
友元函数可以直接访问的类的私有成员,友元函数是定义在类外的普通函数。
- 友元函数可以访问类的私有和保护成员,不能访问类的成员函数
- 友元函数不能够用const修饰
- 友元函数可以在类定义的任何地方进行声明,不受访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用和普通函数的调用原理相同
友元类中的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类的非公有成员。友元关系是单向的不具有交互性。友元关系不能够传递。
class mag
{
friend class wj;
public:
int _id;
int _num;
public:
mag()
:_id(10)
,_num(20)
{}
};
class wj
{
private:
mag m1;
public:
void print_mag()
{
cout << m1._id << endl;
cout << m1._num << endl;
}
};
int main()
{
wj w1;
w1.print_mag();
return 0;
}