#pragma pack(n)
表示的是设置n字节对齐,windows默认是8字节,linux是4字节,鲲鹏是4字节
struct A{
char a;
int b;
short c;
};
- char占一个字节,起始偏移为零,int占四个字节,min(8,4)=4;所以应该偏移量为4,所以应该在char后面加上三个字节,不存放任何东西,short占两个字节,min(8,2)=2;所以偏移量是2的倍数,而short偏移量是8,是2的倍数,所以无需添加任何字节,所以第一个规则对齐之后内存状态为0xxx|0000|00
- 此时一共占了10个字节,但是还有结构体本身的对齐,min(8,4)=4;所以总体应该是4的倍数,所以还需要添加两个字节在最后面,所以内存存储状态变为了 0xxx|0000|00xx,一共占据了12个字节
内存对齐规则
需要对齐的原因
通过类创建一个对象的过程叫实例化,实例化后使用对象可以调用类成员函数和成员变量,其中类成员函数称为行为,类成员变量称为属性。类和对象的关系:类是对象的抽象,对象是类的实例
namespace主要用来解决命名冲突的问题
双冒号::作用域运算符
using分为using声明和using编译指令
using std::cout; //声明
using namespace std; //编译指令
内联函数
C++的函数名称可以重复,称为函数重载。
构造函数和析构函数,分别对应变量的初始化和清理,变量没有初始化,使用后果未知;没有清理,则会内存管理出现安全问题。
当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数
- 构造函数:与类名相同,没有返回值,不写void,可以发生重载,可以有参数,编译器自动调用,只调用一次。
- 析构函数:~类名,没有返回值,不写void,不可以发生重载,不可以有参数,编译器自动调用,只调用一次。
构造函数
析构函数
拷贝构造函数的参数必须加const,因为防止修改,本来就是用现有的对象初始化新的对象。
拷贝构造函数的使用时机
A a; A b = a; A c(a); b = c;//b = c不是初始化,调用赋值运算符
深拷贝和浅拷贝
只有当对象的成员属性在堆区开辟空间内存时,才会涉及深浅拷贝,如果仅仅是在栈区开辟内存,则默认的拷贝构造函数和析构函数就可以满足要求。
我们在定义类或者结构体,这些结构的时候,最后都重写拷贝函数,避免浅拷贝这类不易发现但后果严重的错误产生
- 只能在堆上生成对象:将析构函数设置为私有。
原因:C++是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。- 只能在栈上生成对象:将new 和 delete 重载为私有。
原因:在堆上生成对象,使用new关键词操作,其过程分为两阶段:第一阶段,使用new在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。
将new操作设置为私有,那么第一阶段就无法完成,就不能够在堆上生成对象。
为什么会有this指针
在类实例化对象时,只有非静态成员变量属于对象本身,剩余的静态成员变量,静态函数,非静态函数都不属于对象本身,因此非静态成员函数只会实例一份,多个同类型对象会共用一块代码,由于类中每个实例后的对象都有独一无二的地址,因此不同的实例对象调用成员函数时,函数需要知道是谁在调用它,因此引入了this指针。this指针是对象的首地址。
this指针的作用
this指针是隐含在对象成员函数内的一种指针。当一个对象被创建后,它的每一个成员函数都会含有一个系统自动生成的隐含指针this。this指针指向被调用的成员函数所属的对象(谁调用成员函数,this指向谁),*this表示对象本身,非静态成员函数中才有this,静态成员函数内部没有。
this指针实际上是编译器对非静态成员函数做出的操作,在定义函数时会往函数里传入class *this这个参数,在函数调用时会传入对象的地址。静态成员函数之所以没有this指针是因为静态成员函数先于对象产生,并且是所有对象共享的。
this指针使用
当形参与成员变量名相同时,用this指针来区分
为实现对象的链式引用,在类的非静态成员函数中返回对象本身,可以用return *this,this指向对象,
*this表示对象本身。
void func() const //常函数,此处func为类成员函数
const Person p2; //常对象
合法,但有前提:
sizeof(空class) = 1,为了确保两个不同对象的地址不同。
若将成员变量声明为static,则为静态成员变量,与一般的成员变量不同,无论建立多少对象,都只有一个静态成员变量的拷贝,静态成员变量属于一个类,所有对象共享。静态变量在编译阶段就分配了空间,对象还没创建时就已经分配了空间,放到全局静态区。
不能,静态成员变量最好类内声明,类外初始化。静态成员是单独存储的,并不是对象的组成部分。如果在类的内部进行定义,在建立多个对象时会多次声明和定义该变量的存储位置。在名字空间和作用域相同的情况下会导致重名的问题。
友元主要是为了访问类中的私有成员(包括属性和方法),会破坏C++的封装性,尽量不使用
class Building
{
friend void goodGay(Building * building); //goodGay是Building的友元函数,因此goodGay可以访问building的任意成员
public:
Building(){
m_Sittingroom = "客厅";
m_Bedroom = "卧室";
}
string m_Sittingroom;
private:
string m_Bedroom;
};
//和C语言结构体同,传参时尽量不要传递值,尽量传递指针
void goodGay(Building * building){
cout << "别人在访问" << building->m_Sittingroom << endl;
cout << "别人在访问" << building->m_Bedroom << endl; //当不是友元函数时,不能访问私有成员
}
void test01(){
Building building; //或者Building *build = new Building;这里如果定义指针,需要new,否则未初始化
goodGay(&building);
}
class Building{
friend class Person; //Person是Building的友元函数,因此Person可以访问Building的任意成员
public:
Building(){
this->m_Sittingroom = "客厅";
this->m_Bedroom = "卧室";
}
string m_Sittingroom;
private:
string m_Bedroom;
};
class Person{
public:
void test(Building *building){
cout << building->m_Bedroom << endl;
}
};
void test01(){
Building *build = new Building;//可以在这里写定义,也可以将定义写在Person的构造函数中
Person p;
p.test(build);
}
//和C语言中结构体的互引用类似,需要先声明一个Building类
class Building;
class Person{
public:
void test(Building *building);
void test1(Building *building);
};
class Building{
friend void Person::test1(Building *building); //先将Person类定义,类友元成员函数声明后,再使用friend
public:
Building(){
this->m_Sittingroom = "客厅";
this->m_Bedroom = "卧室";
}
string m_Sittingroom;
private:
string m_Bedroom;
};
//定义Building类后才能定义Person成员函数
void Person::test1(Building *building){
cout << building->m_Bedroom << endl;
}
void Person::test(Building *building){
cout << building->m_Sittingroom << endl;
}
void test01(){
Building *build = new Building;
Person p;
p.test1(build);
}
// 重载 << 运算符
ostream & operator<<(ostream & os, cont A & a) {
// ...
return os;
}
C++内置类型的后置++返回的是变量的拷贝,也就是不可修改的值;前置++返回的是变量的引用,因此可以作为修改的左值。即++(++a)或(++a)++都可以,但++(a++)不可以,(C++默认必须修改a的值,如果不修改则报错)。
//++i
int& int::operator++()
{
*this += 1;
return *this;
}
//i++,注意后置++有占位参数以区分跟前置++不同
const int int::operator++(int)
{
int oldValue = *this;
++(*this);
return oldValue;
}
继承主要是为了减少代码的重复内容,解决代码复用问题。通过抽象出一个基类(父类),将重复代码写到基类中,在派生类(子类)中实现不同的方法。
多继承会产生二义性的问题。如果继承的多个父类中有同名的成员属性和成员函数,在子类调用时,需要指定作用域从而确定父类。
两个子类继承于同一个父类,同时又有另外一个类多继承于两个子类,这种继承称为菱形继承。比如羊和驼继承于动物类,同时羊驼继承于羊和驼。
使用虚继承,在继承方式前加virtual,这样的话羊驼可以直接访问m_Age,不用添加作用域,且这样操作的是共享的一份数据
class Animal{
public:
int m_Age;
};
class Sheep:virtual public Animal{
int m_sheep;
};
class Camel :virtual public Animal{
int m_camel;
};
class Son :public Sheep, public Camel{
int m_son;
};
void test01(){
Son son;
son.m_Age = 10;
cout << sizeof(Animal) << endl; // 4:m_Age
cout << sizeof(Sheep) << endl; // 12:sheep-Vbptr,m_sheep,m_Age
cout << sizeof(Camel) << endl; // 12:camel-Vbptr,m_camel,m_Age
cout << sizeof(Son) << endl; // 24:sheep-Vbptr,m_sheep,camel-Vbptr,m_camel,m_son,m_Age
}
- **特别注意:**此时son没有自己的虚基类表和虚基类指针,只是继承了sheep和camel的虚基类指针和虚基类表,只是修改了两个虚基类表中的值,修改为当前类中,如何通过继承的虚基类指针查找虚基类数据
- Son继承Sheep父类,父类中有虚基类指针vbptr(virtual base pointer),对象结构类似结构体,首元素是虚基类指针,其余为自身数据(不包括静态成员和成员函数)
- Sheep的虚基类指针vbptr指向下面Sheep的虚基类表vbtale@Sheep(virtual base table),虚基类表是一个整型数组,数组第二个元素值为20,即Sheep的虚指针地址偏移20指向Animal的m_Age地址。Camel父类同理,因此,类中只有一个m_Age元素。
- Son中包含了两个指针和四个int类型,所以大小为24。
class Animal{
public:
int m_Age;
};
class Sheep:virtual public Animal{
int m_sheep;
};
class Camel :virtual public Animal{
int m_camel;
};
class Son :virtual public Sheep, virtual public Camel{
int m_son
};
void test01(){
Son son;
son.m_Age = 10;
cout << sizeof(Animal) << endl; // 4:m_Age
cout << sizeof(Sheep) << endl; // 12:sheep-Vbptr,m_sheep,m_Age
cout << sizeof(Camel) << endl; // 12:camel-Vbptr,m_camel,m_Age
cout << sizeof(Son) << endl; // 28:son-vbptr,m_son,m_Age,sheep-Vbptr,m_sheep,camel-Vbptr,m_camel,
}
- 注意跟上面的区别,一个是son类中的元素顺序,一个是son类有了自己的虚基类指针和虚基类表
记录了虚基类数据在派生类对象中与派生类对象首地址(虚基类指针)之间的偏移量
,以此来访问虚基类数据不可以,因为虚函数属于对象,不属于类,静态函数属于类
类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代,如使用子类对象可以直接赋值给父类对象或子类对象可以直接初始化父类对象时,对于同样的一条语句,不管传入子类还是父类对象,都是调用的父类函数,但我们想实现的是同样的一条语句,传入不同的对象,调用不同的函数.
class Animal{
public:
void speak(){
cout << "Animal speak" << endl;
}
};
class Sheep :public Animal{
public:
void speak(){ //重定义,子类重新定义父类中有相同名称的非虚函数
cout << "Sheep speak" << endl;
}
};
void doSpeak(Animal &animal){
animal.speak();
}
//想通过父类引用指向子类对象
void test01(){
Sheep sheep;
doSpeak(sheep); //Animal speak;
sheep.speak(); //sheep speak
sheep.Animal::speak(); //Animal speak; //继承中的重定义可以通过作用域
}
但我们想传入子类对象调用子类函数,传入父类对象调用父类函数,即同样的调用语句有多种不同的表现形态,这样就出现了多态
- 继承
- 虚函数覆盖
- 父类指针或引用指向子类对象访问虚函数
class Animal{
public:
virtual void speak(){ //在父类中声明虚函数,可以实现多态,动态联编
cout << "Animal speak" << endl;
}
int m_age = 0;
};
class Sheep :public Animal{
public:
void speak(){ //发生多态时,子类对父类中的成员函数进行重写,virtual可写可不写
cout << "Sheep speak" << endl;
}
int m_age = 1;
};
void doSpeak(Animal &animal){
animal.speak();
}
void test01(){
//传入子类对象调用子类成员函数
Sheep sheep;
doSpeak(sheep); //sheep speak;
//子类对象直接调用子类成员函数
sheep.speak(); //sheep speak;
//子类对象通过作用域调用父类成员函数
sheep.Animal::speak(); //animal speak;
//基类成员不能转换为子类成员,即不能向下转换
//Animal *animal0 = new Animal();
//Sheep * sheep0 = animal0;
//sheep0->speak();
//同样不能向下转换
//Animal animal0;
//Sheep sheep0 = animal0;
//父类指针指向子类对象
Sheep *sheep1 = new Sheep();
Animal *animal1 = sheep1;
animal1->speak(); //sheep speak;
//父类引用指向子类对象
Sheep sheep2;
Animal &animal2 = sheep2;
animal2.speak(); //sheep speak;
//子类对象直接赋值给父类对象,不符合多态条件,符合类型兼容性原则
Sheep sheep0;
Animal animal0 = sheep0;
animal0.speak(); //animal speak;
}
- 静态多态(运算符重载、函数重载)
- 动态多态(继承、虚函数)
两者主要的区别:函数地址是早绑定(静态联编)还是晚绑定(动态联编)。即,在编译阶段确定好地址还是在运行时才确定地址。
- 前提发生了多态,每个类中都有虚函数表,最开始的父类创建虚函数表,后面的子类继承父类的虚函数表,然后对虚函数重写
- 虚函数重写(覆盖)的实质就是重写父类虚函数表中的父类虚函数地址;
- 实现多态的流程:虚函数指针->虚函数表->函数指针->入口地址,虚函数表(vftable)属于类,或者说这个类的所有对象共享一个虚函数表;虚函数指针(vfptr)属于单个对象。
- 在程序调用时,先创建对象,编译器在对象的内存结构头部添加一个虚函数指针,进行动态绑定,虚函数指针指向对象所属类的虚函数表。
- 虚函数表是一个指针数组,其元素是虚函数的指针,每个元素对应一个函数的指针。如果子类对父类中的一个或多个虚函数进行重写,子类的虚函数表中的元素顺序,会按照父类中的虚函数顺序存储,之后才是自己类的函数顺序。
- 编译器根本不会去区分,传进来的是子类对象还是父类对象,而是关心调用的函数是否为虚函数。如果是虚函数,就根据不同对象的vptr指针找属于自己的函数。父类对象和子类对象都有vfptr指针,传入对象不同,编译器会根据vfptr指针,到属于自己虚函数表中找自己的函数。即:vptr—>虚函数表------>函数的入口地址,从而实现了迟绑定(在运行的时候,才会去判断)。
指针函数int* f(int x, int y)
本质是函数,返回值为指针,函数指针int (*f)(int x)
本质是指针,指向函数的指针
通常我们可以将指针指向某类型的变量,称为类型指针(如,整型指针)。若将一个指针指向函数,则称为函数指针。
函数名代表函数的入口地址,同样的,我们可以通过根据该地址进行函数调用,而非直接调用函数名。
void test001(){
printf("hello, world");
}
int main(){
void(*myfunc)() = test001;//将函数写成函数指针
myfunc(); //调用函数指针 hello world
}
test001的函数名与myfunc函数指针都是一样的,即都是函数指针。test001函数名是一个函数指针常量,而myfunc是一个函数指针变量,这是它们的关系。
可以让函数指针指向参数类型相同、返回值类型也相同的函数。通过函数指针我们也可以实现C++中的多态。
#include
typedef int (*func)();
int print1(){
printf("hello, print1 \n");
return 0;
}
int print2(){
printf("hello, print2 \n");
return 0;
}
int main(int argc, char * argv[]){
func fp = print1;
fp();
fp = print2;
fp();
return 0;
}
多态的实现主要分为静态多态和动态多态,静态多态主要是重载,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定。
举个例子:一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数
两个问题本质是一样的,构造函数不能实现多态
对象在创建时,由编译器对VPTR指针进行初始化,只有当对象的构造完全结束后VPTR的指向才最终确定。
子类中虚函数指针的初始化过程
当定义一个子类对象的时候比较麻烦,因为构造子类对象的时候会首先调用父类的构造函数然后再调用子类的构造函数。当调用父类的构造函数的时候,此时会创建Vptr指针,该指针会指向父类的虚函数表;然后再调用子类的构造函数,子类继承父类的虚函数指针,此时Vptr又被赋值指向子类的虚函数表。
也就是说,会先调用父类构造函数,再调用子类构造函数,并不会只调用子类构造函数,是没法实现多态的
不能,因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针
第一个原因
,在概念上,构造函数的工作是为对象进行初始化。在构造函数完成之前,被构造的对象被认为“未完全生成”。当创建某个派生类的对象时,如果在它的基类的构造函数中调用虚函数,那么此时派生类的构造函数并未执行,所调用的函数可能操作还没有被初始化的成员,这将导致灾难的发生。
第二个原因
,即使想在构造函数中实现动态联编,在实现上也会遇到困难。这涉及到对象虚指针(vptr)的建立问题。在Visual C++中,包含虚函数的类对象的虚指针被安排在对象的起始地址处,并且虚函数表(vtable)的地址是由构造函数写入虚指针的。所以,一个类的构造函数在执行时,并不能保证该函数所能访问到的虚指针就是当前被构造对象最后所拥有的虚指针,因为后面派生类的构造函数会对当前被构造对象的虚指针进行重写,因此无法完成动态联编。
#include
using namespace std;
class A{
public:
A() {show();}
virtual void show(){
cout<<"in A"<
析构函数是用来销毁一个对象的,在销毁一个对象时,先调用该对象所属类的析构函数,然后再调用其基类的析构函数,所以,在调用基类的析构函数时,派生类对象的“善后”工作已经完成了,这个时候再调用在派生类中定义的函数版本已经没有意义了。
#include
using namespace std;
class A{
public:
virtual void show(){
cout<<"in A"<
在程序设计中,如果仅仅为了设计一些虚函数接口,打算在子类中对其进行重写,那么不需要在父类中对虚函数的函数体提供无意义的代码,可以通过纯虚函数满足需求。
纯虚函数的语法格式:virtual 返回值类型 函数名 () = 0;
只需要将函数体完全替换为 =0即可,纯虚函数必须在子类中进行实现,在子类外实现是无效的。
注意
- 仅仅发生继承时,创建子类对象后销毁,函数调用流程为:父类构造函数->子类构造函数->子类析构函数->父类析构函数;
- 当发生多态时(父类指针或引用指向子类对象),通过父类指针在堆上创建子类对象,然后销毁,调用流程为:父类构造函数->子类构造函数->父类析构函数,不会调用子类析构函数,因此子类中会出现内存泄漏问题。
解决方法:将父类中的析构函数设置为虚函数,设置后会先调用子类析构函数,再调用父类析构函数
因为当发生多态时,父类指针在堆上创建子类对象,销毁时会内存泄漏
因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存
通过template或template实现,主要用于数据的类型参数化,简化代码,有类模板和函数模板,函数模板是用于生成函数的,类模板则是用于生成类的
- template声明下面是函数定义,则为函数模板,否则为类模板。
- 注意:每个函数模板前必须有且仅有一个template声明,不允许多个template声明后只有一个函数模板,也不允许一个template声明后有多个函数模板(类模板同理)。