站在编译器和C的角度剖析c++原理, 用代码说话
默认构造函数
我们先回顾一下构造函数, 当我们没有指定带参的构造函数时,编译器会给我们的类自动分配一个默认的无参构造函数.怼代码:
class Test{
public:
Test(){
cout << "我是构造函数,自动被调用了" << endl;
}
~Test(){
cout << "我是析构函数,自动被调用了";
}
protected:
private:
int a;
};
//单独搭建一个舞台,让对象唱戏, 这样可以完整的看见对象声明周期的呈现
void ObjPlay()
{
Test t1;
cout << "展示t1的生命周期" << endl;
}
int main(int argc, const char * argv[]) {
ObjPlay();
return 0;
}
~Test()
这个表示的就是析构函数,当 Test t1;
的作用域执中的代码执行完后就开始调用析构函数进行资源回收. 这只是个简单的例子,我们接下来会陆续有复杂的例子,跟进喽~~.
拷贝构造函数
copy构造函数又叫做复制构造函数, 这是重点也是个难点,也是C++的特有的重要特点,我会循序渐进的讲解清晰. 首先要知道拷贝构造函数也是一种构造函数,也就是说如果我们没有指定代参的构造函数而是指定了拷贝构造函数,那么系统也不再会给我们分配默认的无参构造函数.
我们上代码进行分析:
class AA
{
public:
AA()
{
a = 5;
cout<<"我是构造函数,自动被调用了"<<endl;
}
AA(const AA &obj2)
{
cout<<"我也是构造函数,我是通过另外一个对象obj2,来初始化我自己"<<endl;
a = obj2.a + 10;
}
~AA()
{
cout<<"我是析构函数,自动被调用了"<<endl;
}
void getA()
{
printf("a:%d \n", a);
}
protected:
private:
int a;
};
void ObjPlay01()
{
AA a1;
AA a2 = a1;
//a2 = a1;
}
int main()
{
ObjPlay01();
return 0;
}
我们通过拷贝构造函数的语法格式上可以看出const AA &obj2
就是用一个对象初始化另一个对象. 首先AA a1;
创建了一个AA对象, AA a2 = a1;
当我们执行这句话的时候,就是用一个对象初始化了另外一个对象,注意初始化的意思就是边创建边赋值这种形式. 我们从内存中分析这一过程: 这里先说明一点,我们在后面会介绍到c++的操作符, 就是等号等等. c++的这些符号很有意思, 这里只涉及到了等号,那么我们就事论事. 这个等号很有意思. 那么这个过程是什么样的呢? AA a2 = a1;
一执行,那么就会调用a2的拷贝构造函数, 在这之前的栈中已经有了a2对象的a属性了, 然后也有a1对象的a属性,并且a1对象的a属性已经是5了,因为a1执行了无参构造函数, 但是a2的a还是0. 那么a1就是obj2
. 然后执行了运算后, this(也就是a2)对象的a已经变成了15,在这里我们发现a2创建的时候没有调用无参构造函数,这就是拷贝构造函数高效率的地方. 如果我们放开注释的a2 = a1
那么这种等于号的操作不是对象初始化的操作,那么就是简单的值拷贝,a1栈中的值给了a2, 这也就是说的浅拷贝. 我们再看一种场景:
class AA
{
public:
AA(int _a) //无参构造函数 默认构造函数
{
a = _a;
}
~AA()
{
cout<<"我是析构函数,自动被调用了"<<endl;
}
void getA()
{
printf("a:%d \n", a);
}
protected:
private:
int a;
};
void ObjPlay02()
{
AA a1(10);
AA a2(a1); //定义变量并初始化
//a2 = a1; //用a1来=号给a2 编译器给我们提供的浅copy
a2.getA();
}
int main()
{
ObjPlay02();
return 0;
}
在这种场景下我们是用AA a2(a1);
这种方式进行初始化的,与上面的AA a2 = a1;
是一样的. 并且这个案例中没有了拷贝构造函数,那么当我们执行AA a2(a1);
时,按理说就应该去执行拷贝构造函数,那么编译器就会给我们隐士的走一个拷贝构造函数,但是这个函数也是简单浅拷贝,但是这样也就能使得第二个对象不需要走构造非拷贝构造函数的构造函数. 接下来就是析构了, 先初始化的后析构,所以会先析构a2,然后析构a1. 当然都是在执行完a2.getA()
后执行的.
对拷贝构造函数有了初步认识后,我们再接个案例:
class Location
{
public:
Location( int xx = 0 , int yy = 0 )
{
X = xx ; Y = yy ; cout << "Constructor Object.\n" ;
}
Location( const Location & p )//复制构造函数
{
X = p.X ; Y = p.Y ; cout << "Copy_constructor called." << endl ;
}
~Location()
{
cout << X << "," << Y << " Object destroyed." << endl ;
}
int GetX () { return X ; } int GetY () { return Y ; }
private : int X , Y ;
} ;
void f ( Location p )
{
cout << "Funtion:" << p.GetX() << "," << p.GetY() << endl ;
}
void mainobjplay()
{
Location A ( 1, 2 ) ;
f ( A ) ;//调用了拷贝构造函数
}
int main()
{
mainobjplay();
return 0;
}
前面的就不重复了,f ( A ) ;
这句话对应的函数中的Location p
, 注意形参中的类型不是引用也不是指针,是一个对象,所以就和上面的初始化一样了,那么就直接调用拷贝构造函数就行. 然后析构两次,第一次是从void f这个函数跳出来也就是f(A)执行完的时候析构一次,也就是析构P,然后析构A.
匿名对象
对拷贝构造函数有了更进一步的认识后,我们需要引入一个叫匿名对象的东东,挺恶心的,跟紧了~~:
class Location
{
public:
Location( int xx = 0 , int yy = 0 )
{
X = xx ; Y = yy ; cout << "Constructor Object.\n" ;
}
Location( const Location & p ) //复制构造函数
{
X = p.X ; Y = p.Y ; cout << "Copy_constructor called." << endl ;
}
~Location()
{
cout << X << "," << Y << " Object destroyed." << endl ;
}
int GetX () { return X ; } int GetY () { return Y ; }
private : int X , Y ;
} ;
void f ( Location p )
{
cout << "Funtion:" << p.GetX() << "," << p.GetY() << endl ;
}
Location g()//注意这里返回的不是指针不是引用, 是一个对象
{
Location A(1, 2);
return A;
}
void mainobjplay()
{
Location B;
B = g();
//Location B = g();
int main()
{
mainobjplay();
return 0;
}
这段代码与之前的不同之处在于Location g()
这个函数, 这里返回的不是引用也不是指针,返回的是个对象,但是一旦出了作用于就回收了啊,那么编译器和之前咱们讨论到的返回的东西一样,给临时区分配了一个这个对象的匿名对象,然后针对匿名对象,这里连析构都没调用,当用B=g()
的时候,栈中的A对象析构,因为匿名对象还在,然后这个匿名对象并没有走拷贝构造函数,而是直接进行了等号的浅拷贝直接给了B对象的对应变量赋值.然后匿名对象直接挂掉. 这样我们只进行了两次有参构造函数和两次析构. 当我们用Location B = g();
的时候,就总共进行了一次有参构造和一次析构. 这是为什么呢?不是应该A也要析构吗?这就是我上面稍微提到过的对象初始化问题,后者就是在对象初始化的时候进行匿名对象拷贝的,但是前者并不是初始化的情况. 当初始化的时候,相当于要析构的A上面覆盖了B了,然后返回的匿名对象进行浅拷贝. 之前为什么不是因为B在A之前已经有了,所以两者地址不一样.
接下来我们再加强一下拷贝构造函数的深浅拷贝问题:
class Name
{
public:
Name(const char *pname)
{
size = strlen(pname);
pName = (char *)malloc(size + 1);
strcpy(pName, pname);
}
~Name()
{
cout<<"开始析构"<<endl;
if (pName!=NULL)
{
free(pName);
pName = NULL;
size = 0;
}
}
protected:
private:
char *pName;
int size;
};
void playObj()
{
Name obj1("obj1.....");
Name obj2 = obj1; //obj2创建并初始化
Name obj3("obj3...");
cout<<"业务操作。。。5000"<<endl;
}
int main()
{
playObj();
return 0;
}
这段代码中我们没有指定拷贝构造函数,那么就会调动编译器提供的默认拷贝构造函数,那么当在执行第三次析构的时候(也就是析构obj1)的时候程序就崩了. 为什么?我上面也已经提到过了,默认的拷贝构造函数是浅拷贝,具体流程是这样的: 一开始执行到方法playObj的时候,栈中创建了obj1, obj2, obj3, 然后obj1开始调用有参构造函数并且pName中存储了堆区分配好的首地址. 然后进行初始化的时候调用拷贝构造函数,这是个浅拷贝,只是将栈内存中的值进行值进行了拷贝,也就是拷贝了那个堆区的首地址给了obj的pName. 换句话说就是它俩指向了同一块儿堆区. 然后obj3结束后先析构obj3,接着析构obj2,当析构obj1的时候堆中已经非法了.所以我们不能再用系统给的浅拷贝的默认拷贝构造函数了, 而是需要重新为2分配一块儿内存:
Name(Name &obj)
{
//用obj来初始化自己
pName = (char *)malloc(obj.size + 1);
strcpy(pName, obj.pName);
size = obj.size;
}
当我们执行obj2 = obj3;
的话,程序依然是蹦,原因就如上一下都是浅拷贝,所以我们需要对这个等号操作, 我们下一张就会涉及到对操作符的重写. 这里先题一下:
void operator=(Name &obj3)
{
if (pName != NULL)
{
free(pName);
pName = NULL;
size = 0;
}
cout<<"测试有没有调用我。。。。"<<endl;
pName = (char *)malloc(obj3.size + 1);
strcpy(pName, obj3.pName);
size = obj3.size;
}
这里有个问题就是到底有没有必要先释放一波内存,我觉得是没有必要的,有的人会说内存泄露问题,但是我通过内存观察并没有发现有内存泄露问题,并且我们obj2分配了新的内存,并没有影响到obj3的pName的指向问题啊,只是用了它里面的值啊. 所以说如果有大神看见批评两句然后呈上您的解析~~~~万分感谢………
到这里我们就把拷贝构造函数的情况基本涉及到了,那么总结一下吧:
拷贝构造函数规则研究:
1. 当类中没有定义任何一个构造函数的时候,c++编译器会提供无参构造函数和拷贝构造函数
2. 当类中定义了任意的非拷贝构造函数(无参,有参), c++编译器不会提供无参构造函数
3. 当类中定义了拷贝构造函数, c++编译器不会提供午餐构造函数
4. 默认的拷贝构造函数成员变量简单赋值.
构造函数的初始化列表
先上代码其中就包含了语法规则:
class A
{
public:
A(int _a1)
{
a1 = _a1;
}
protected:
private:
int a1;
};
//构造函数的初始化列表产生原因
//语法现象
class B
{
public:
B():mya(12),mya2(100)
{
;
}
//成员变量的初始化顺序与声明的顺序相关,与在初始化列表中的顺序无关
B(int x, int y):mya(y),mya2(101)
{
b1 = x;
}
protected:
private:
int b1;
A mya2;
A mya;
};
int main()
{
A a1(10);
B b1(10, 20);
return 0;
}
当我们在初始化B的时候,B中的成员属性有类A, 所有需要对A进行初始化,这样就出现了初始化列表. 执行步骤是先初始化mya2(101)
, 接着mya(y)
,最后b1 = x;
. 当析构的时候,先析构自己本身,然后析构A mya
, 最后析构A mya2
.
初始化列表我们再来一段代码然后总结一下:
class ABC
{
public:
ABC(int a, int b, int c)
{
this->a = a;
this->b = b;
this->c = c;
printf("a:%d,b:%d,c:%d \n", a, b, c);
printf("ABC construct ..\n");
}
~ABC()
{
printf("a:%d,b:%d,c:%d \n", a, b, c);
printf("~ABC() ..\n");
}
protected:
private:
int a;
int b;
int c;
};
class MyD
{
public:
MyD():abc1(1,2,3),abc2(4,5,6),m(100)
{
cout<<"MyD()"<<endl;
}
~MyD()
{
cout<<"~MyD()"<<endl;
}
protected:
private:
ABC abc1; //c++编译器不知道如何构造abc1
ABC abc2;
const int m;
};
int run()
{
MyD myD;
return 0;
}
int main()
{
run();
return 0;
}
使用初始化列表出现原因:
1. 如果我们有一个类成员,它本身是一个类或者是一个结构,而且这个成员它只有一个带参数的构造函数,而没有默认构造函数,这时要对这个类成员进行初始化,就必须调用这个类成员的带参数的构造函数,如果没有初始化列表,那么他将无法完成第一步,就会报错。
2. 类成员中若有const修饰,必须在对象初始化的时候,给const int m 赋值, 当类成员中含有一个const对象时,或者是一个引用时,他们也必须要通过成员初始化列表进行初始化,因为这两种对象要在声明后马上初始化,而在构造函数中,做的是对他们的赋值,这样是不被允许的。
拷贝构造函数加强
先上案例:
class ABCD
{
public:
ABCD(int a, int b, int c)
{
this->a = a;
this->b = b;
this->c = c;
printf("ABCD() construct, a:%d,b:%d,c:%d \n", this->a, this->b, this->c);
}
~ABCD()
{
printf("~ABCD() construct,a:%d,b:%d,c:%d \n", this->a, this->b, this->c);
}
int getA()
{
return this->a;
}
protected:
private:
int a;
int b;
int c;
};
class MyE
{
public:
MyE():abcd1(1,2,3),abcd2(4,5,6),m(100)
{
cout<<"MyD()"<<endl;
}
~MyE()
{
cout<<"~MyD()"<<endl;
}
MyE(const MyE & obj):abcd1(7,8,9),abcd2(10,11,12),m(100)
{
printf("MyD(const MyD & obj)\n");
}
protected:
//private:
public:
ABCD abcd1;
ABCD abcd2;
const int m;
};
int doThing(MyE mye1)
{
printf("doThing() mye1.abc1.a:%d \n", mye1.abcd1.getA());
return 0;
}
int run2()
{
MyE myE;//这行会调用三个构造函数
doThing(myE);//会执行拷贝构造函数
return 0;
}
int run3()
{
printf("run3 start..\n");
ABCD abcd = ABCD(100, 200, 300);
//ABCD(400, 500, 600); //临时对象的生命周期, 构造函数后直接就执行析构了,因为这个临时对象c++发现没用.
printf("run3 end\n");
return 0;
}
int main()
{
//run2();
run3();
return 0;
}
run2()就不进行解释了,run3()中ABCD abcd = ABCD(100, 200, 300);
就是典型的用匿名对象进行初始化, 这样,只执行一次有参构造函数,然后一次析构,因为这是初始化. 置于后面注释的ABCD(400, 500, 600);
如果只执行这句的话,就是进行有参构造后,直接进行了析构,因为编译器发现这个匿名对象并没有用,所以会直接析构.
就像这段代码:
class MyTest
{
public:
MyTest(int a, int b, int c)
{
this->a = a;
this->b = b;
this->c = c;
}
MyTest(int a, int b)
{
this->a = a;
this->b = b;
MyTest(a, b, 100);
}
~MyTest()
{
printf("MyTest~:%d, %d, %d\n", a, b, c);
}
protected:
private:
int a;
int b;
int c;
public:
int getC() const { return c; }
void setC(int val) { c = val; }
};
int main()
{
MyTest t1(1, 2);
printf("c:%d", t1.getC()); //乱码
system("pause");
return 0;
}
为什么会乱码呢,因为MyTest(a, b, 100);
这个匿名函数编译器发现并没有用到所以直接进行了析构. 所以对c根本没有操作.
new delete
先上案例进行分析:
int main()
{
//new基础类型
int *p = (int *)malloc(sizeof(int));
free(p);
int *p2 = new int; //相当于上面的
*p2 = 101;
printf("*p2:%d \n", *p2);
delete p2;
//分配内存的同时,初始化
int *p3 = new int(100);
delete p3;
return 0;
}
int main()
{
//new数组
int *p = (int *)malloc(10*sizeof(int)); //int a[10];
p[0] = 1;
free(p);
int *p2 = new int[10] ;//int a[10];
p2[0] = 1;
p2[1] = 2;
delete [] p2;
return 0;
}
//new 对象
class Test
{
public:
Test(int mya, int myb)
{
a = mya;
b = myb;
cout<<"构造函数,我被调用了"<<endl;
}
~Test()
{
cout<<"析构函数,我被调用了"<<endl;
}
int getA()
{
return a;
}
protected:
private:
int a;
int b;
};
int getTestObj(Test **my)
{
Test *p = new Test(1, 2);
*my = p;
return 0;
}
int main()
{
//new + 类型 返回一个 内存首地址
//new操作符也会自动的调用这个类的构造函数
//delete自动的调用这个类的析构函数
//相当于我们程序员可以手工控制类的对象的生命周期
Test *p = new Test(1, 2);//这个内存分配在了堆上
cout<<p->getA()<<endl;
getTestObj(&p);//我们也可以传入一个Test指针然后让函数去帮我们初始化对象
delete p;
return 0;
}
malloc不会调用这个类的构造函数.
static
上案例代码:
class BB
{
public:
int getC()
{
return c;
}
void setC(int nyc)
{
c = nyc;
}
//静态成员函数是属于整个类,
static void getMem()
{
//cout<<a<<endl;是因为不知道输出哪个对象的a,这个a应该是属于具体的对象的a, 但是getMem是全局的
cout<<c<<endl;
}
protected:
private:
int a;
int b;
static int c;
};
int BB::c = 0;
//static修饰的变量,是属于类,所有的对象都能共享用。
void main111()
{
BB b1;
BB b2;
cout<<b2.getC()<<endl;;
b1.setC(100);
cout<<b2.getC()<<endl;;
system("pause");
}
int main()
{
//调用静态成员函数的方法1
BB::getMem();
//调用静态成员函数的方法2
BB b1;
b1.getMem();
return 0;
}
由上面代码我们可以知道,静态成员函数是属于整个类, static修饰的变量,是属于类,所有的对象都能共享用. 当使用静态成员函数的时候,操作的成员变量也必须是静态的,因为静态成员函数是放在静态区的,并不是和类放在栈中的,所以当我们创建不同的类的时候,静态的成员函数和成员变量并不会变化,所以不同的对象调用的时候,它们区分不出来是哪个对象在调用,所以静态成员函数只能使用静态成员变量. 再有就是调用静态成员函数的方法其中一种是:BB::getMem();
,你或许会有疑问说这谁调用的啊,显示谁的啊? 记住这是静态成员,你什么时候调用就是直接调用静态区的值. 没有关联任何的对象.
有人会问说类的内存四区示意图是什么样子呢?我们在后面有一个专题会讨论这个,这里先给大家测试一下类到底占用多大内存:
class C1
{
public:
int i; //4
int j; //4
int k; //4
protected:
private:
}; //12
class C2
{
public:
int i; //4
int j; //4
int k; //4
static int m;
public:
int getK() const { return k; }
void setK(int val) { k = val; }
protected:
private:
};
int main()
{
C2 c21, C22,C23;
c21.getK();
//c22.getK();
printf("sizeof(C1):%d \n", sizeof(C1));
printf("sizeof(C2):%d \n", sizeof(C2));
return 0;
}
C1的大小就不说了,C2的大小也是12, 这说明了成员变量和成员函数不在一起分配的.
成员变量:
普通成员变量:存储于对象中,与struct变量有相同的内存布局和字节对齐方式
静态成员变量:存储于全局数据区中
成员函数:存储于代码段中。
问题出来了:很多对象公用一块代码?代码是如何区分具体对象的那?引出了我们接下来的话题
this 初探
学到这里,那么我们有个疑问,当我们调用类中的成员方法的时候用b1.getCount()
这种方式,不同的对象都用这种方式去调用,那么类中是如何识别是属于哪个类的呢?
class Test
{
public:
Test(int a, int b)
{
this->a = a;
this->b = b;
}
void getA() //getA(this);
{
printf("a:%d \n", this->a);
printf("a:%d \n", a);
}
protected:
private:
int a;
int b;
};
int main()
{
Test t(1,2); //====>Test(this, 1, 2);===>Test(&t, 1, 2);
return 0;
}
- C++类对象中的成员变量和成员函数是分开存储的。C语言中的内存四区模型仍然有效!
- C++中类的普通成员函数都隐式包含一个指向当前对象的this指针。
- 静态成员函数、成员变量属于类
静态成员函数与普通成员函数的区别
静态成员函数不包含指向具体对象的指针
普通成员函数包含一个指向具体对象的指针
联系方式: reyren179@gmail.com