【1】空类为什么可以创建对象呢? 示例代码如下: 1 class Test 2 { 3 }; 4 void main() 5 { 6 Test t1; 7 cout<<sizeof(Test)<<endl; //1 8 } 让我们先看看这个例子。既然都没有构造函数,怎么实现对象t1的构建呢? 哦,经过大脑的回旋式搜索,忆得有一本书上说过,当用户定义一个空类(如上)时,编译器就会为这个类默认生成六个方法。 既然这是编译器完成的工作,那我们只要知道是那些方法就好了,其余就学习编译器的结构以及原理了。 那么,编译器生成了那六个方法:示例代码如下: 1 class Empty 2 { 3 public: 4 Empty(); //默认构造方法 5 Empty(const Empty &); //拷贝构造函数 6 ~Empty(); //析构函数 7 Empty &operator=(const Empty &); //赋值函数 8 Empty *operator &(); //取地址 9 const Empty * operator&() const; //常对象取地址 10 }; OK,这就是默认生成的那六个方法。其中前四个是经常谈及到的,也是平常定义一个类时,尤其要慎重考虑的。 到这里,也许有人还看出了点问题。那就是为什么空类的大小是1呢??呵呵~~编译器搞的鬼?其实也不是随随便便搞的鬼,我们说:存在即是合理的!这样做肯定有它的道理。 大家试想一个极端问题,现在还是上面这个空类,我们定义一个语句:Test ar[10];一个对象数组。 好啦,如果sizeof(Test)==0,那么我们如何区分数组的十个元素??你想想这不是狭隘的表现吗? 所以说,为了更完善,更健壮,更伟大。编译器插入这个字节,是为了再使用这个类定义多个对象的时候能保证每个对象有自己的地址。 另外,大家看看这个例子: 1 A *pa1,pa2; 2 pa1 = new A(); 3 pa2 = new A(); 4 .. 5 if(pa1 == pa2) //如果不分配内存,这个比较就会失去意义 6 { 7 } 【2】构造函数注意哪些知识点? (1)构造函数 构造函数是一种特殊的方法,主要用来在创建对象时初始化对象,即为对象的属性(数据成员)赋初始值,总与new运算符一起使用在创建对象的语句中。 在C++重载的机制下,一个类可以有多个构造函数,可根据其参数个数的不同或参数类型的不同或参数顺序的不同来区分它们,即构造函数的重载。 (2)构造函数注意事项: <1>构造函数的函数名必须与类名相同,而且没有返回值,更不能用void来修饰。默认访问权限是public的,并且inline函数【常识】 <2>构造函数允许重载。【各个不同的构造函数创建的对象就是所谓的天生不同性】 <3>构造函数不能被直接调用,必须通过new运算符在创建对象时才会自动调用。 <4>当一个类没有定义构造函数时,编译器会为每个类添加一个默认的构造函数。 <5>在一个类中,只要有用户自定义的一个构造函数,那么这个类的默认构造函数就不再存在。 那么也就意味着,欲实现想默认构造函数那样创建对象的形式,必须另外添加一个默认构造函数,或者用户定义一个复合默认构造函数。 准确理解请看以下示例代码: 1 /* 2 *默认构造函数创建对象 3 */ 4 class Test 5 { 6 private: 7 int a; 8 }; 9 void main() 10 { 11 Test t1; //默认构造函数创建对象 12 } 1 /* 2 *自定义构造函数 3 */ 4 class Test 5 { 6 private: 7 int a; 8 public: 9 Test(int x):a(x) 10 {} 11 }; 12 void main() 13 { 14 // Test t1; //error!!编译器提示:no appropriate default constructor available 15 Test t2(100); //OK 因为有相应类型的构造函数(自定义构造函数) 16 } 1 /* 2 *复合默认构造函数 3 */ 4 class Test 5 { 6 private: 7 int a; 8 public: 9 //Test(){}; //error!! 编译时出错,因为下面的这个是复合默认构造函数,二者冲突 10 Test(int x=10):a(x){}; 11 }; 12 void main() 13 { 14 Test t1; //OK 因为构造函数有默认值 15 Test t2(100); //OK 因为有相应类型的构造函数 16 } <6>构造函数一般不建议定义为私有的,因为在外部无法实现对象的创建。但是,特殊需要的情况下也是可以的这样实现的。 <7>构造函数有两种方式实现对数据成员的初始化。 1:使用初始化列表进行初始化【第五点中示例代码可见】 2:在函数体中进行初始化 <8>在用默认构造函数创建对象时,如果创建的是全局对象或静态对象,则对象的位模式全为 0,否则,对象值是随机的。 <9>基类构造函数负责构建基类的对象,派生类的构造函数负责构建派生类的对象。派生类对象创建是由基类开始进行的。 <10>构造函数不可以声明为const,也是完全没有必要的 <11>注意分清楚对象与函数声明问题。示例代码如下: 1 class Test 2 { 3 private: 4 int a; 5 public: 6 Test(){}; 7 }; 8 void main() 9 { 10 Test t1; //对象? 11 Test t2(); //对象?函数? 12 } 注意:第十一行:编译器是以函数声明进行编译的,不是一个对象! 【3】析构函数注意哪些知识点? (1)析构函数 我们已经了解了C++的编程哲学观是面向对象的,那么程序在运行时,创建了很多的对象,而这些对象在当初系统创建时,是为各自都分配有一定的内存空间的。 现在,程序要结束了!我们就要释放我们所建立的这些对象所占用的内存资源。而析构函数就是完成这些善后工作的。 (2)析构函数注意事项: <1>构造函数的函数名必须与类名相同,与构造相反再加一个非(~)。而且无参数没有返回值,更不能用void来修饰。 默认访问权限是public的,并且inline函数【常识】 <2>析构函数不可以重载,一个类中只允许有一个析构函数 <3>建立对象时若用new开辟了一片内存空间,应在退出前在析构函数中用delete全部释放 <4>析构函数仅仅只是一个普通成员函数 <5>new 与 delete操作符详解 请看下面的示例代码以及说明: new操作符使用示例: Test *ptr = new Test(); 这一条语句,new操作符完成了两件事。 1:分配足够的内存以便容纳所需类型的对象。 2:调用构造函数初始化内存中的对象。 而且,你始终记住,new操作符的使命,或者说赋予它的能力就是这么大,只完成这两件事情,不能以任何方式改变它的行为。 delete操作符使用示例: delete ptr; 这一条语句,delete操作符完成了两件事。 1:调用对象的析构函数,释放对象申请的堆内存。 2:释放对象本身的内存,彻底释放本次申请的资源。 另外,请看如下两段代码: 代码1: 1 #include<iostream> 2 using namespace std; 3 class Test 4 { 5 public: 6 Test() 7 { 8 p = new char[1024]; 9 } 10 ~Test() 11 { 12 cout<<"deconstructor"<<endl; 13 delete []p; 14 p = NULL; 15 } 16 private: 17 char *p; 18 }; 19 void main() 20 { 21 Test *pa = new Test(); 22 pa->~Test(); //? ? ? 23 } 代码2: 1 #include<iostream> 2 using namespace std; 3 class Test 4 { 5 public: 6 Test() 7 { 8 p = new char[1024]; 9 } 10 ~Test() 11 { 12 cout<<"deconstructor"<<endl; 13 delete []p; 14 p = NULL; 15 } 16 private: 17 char *p; 18 }; 19 void main() 20 { 21 Test *pa = new Test(); 22 delete pa; //? ? ? 23 } 分析:注意每个示例的第22行: 第一个:仅仅调用了析构函数,析构函数释放了申请的堆内存。而对象本事的内存,明显是由系统回收了。 第二个:delete就隐式的调用了析构函数,并且释放了pa所指向的对象本身内存。最后main函数退出前,摧毁pa指针。 其实,这个道理很简单的。举个现实的例子:见过盖房子和拆房子吧?申请内存就像买地基,构建对象就类似盖房子,析构就像拆房子,而pa就像你家的房地产证。 pa指向当初买的地盘,那肯定是比你盖房子的那块地皮要大得多! 所以说这也就是问题的关键,我们要归还的不仅仅是你房子占的那么大面积,而是要全部释放掉你当初申请的那么大块的面积。 【4】拷贝构造函数注意哪些知识点? (1)为什么需要拷贝构造函数? 因为我们想实现用一个已知对象去创建一个与它完全相同的新对象 (2)拷贝构造函数有什么特点? <1>函数名与类名相同,并且没有返回值类型 <2>有且仅有一个参数,并且是同类已知对象的引用 <3>每个类必有一个拷贝构造函数,如果programer没有定义,系统会添加默认拷贝构造函数 (3)拷贝构造的参数为什么是对象引用? 如果不是的话,将导致无限递归..... (4)拷贝构造函数一般在什么时候使用? <1>声明一个新的对象时,用已知对象初始化新对象。 示例代码如下: 1 #include<iostream> 2 using namespace std; 3 4 class Test 5 { 6 private: 7 int a; 8 public: 9 Test(int x=0):a(x) 10 { 11 cout<<"construction"<<endl; 12 } 13 Test(const Test & t) 14 { 15 cout<<"Copy construction"<<endl; 16 a=t.a; 17 } 18 }; 19 20 void main() 21 { 22 Test t1; //调用构造函数 23 Test t2(t1); //调用拷贝构造函数 24 Test t3 = t2; //调用拷贝构造函数 25 } 26 /* 27 construction 28 Copy construction 29 Copy construction 30 */ <2>当一个已知对象作为一个实参传递给一个函数的形参时,在用实参对象初始化形参对象时,需要调用拷贝构造函数。 <3>当对象作为一个函数的返回值时,要创建临时对象,临时对象的初始化是调用拷贝构造函数进行的。 关于<2>,<3>点示例代码如下: 1 #include<iostream> 2 using namespace std; 3 4 class Test 5 { 6 private: 7 int a; 8 public: 9 Test(int x = 0):a(x) 10 { 11 cout<<"construction"<<endl; 12 } 13 Test(const Test & obj) 14 { 15 cout<<"Copy construction"<<endl; 16 a = obj.a; 17 } 18 }; 19 20 Test Fun(Test t) 21 { 22 return t; 23 } 24 25 void main() 26 { 27 Test t1; //调用构造函数 28 Test t2(t1); //调用拷贝构造函数 29 Fun(t2); //两次调用拷贝构造函数 30 } 31 32 //the result of this: 33 /* 34 construction 35 Copy construction 36 Copy construction 37 Copy construction 38 */ <4>在以下两种方式下也是应用拷贝构造函数的 1:在初始化顺序容器中的元素时 2:根据元素初始化列表初始化数组元素 (5)什么是浅拷贝?什么是深拷贝? 其实,浅拷贝深拷贝是很简单的问题。这样说话有点欠拍砖!因为会的人什么都觉得简单。呵呵!请看下面的分析: 想象一下:假如一个类中的数据成员有一个是指针类型的,那么,用户创建一个对象,系统就要为这个对象的指针成员对应的分配一块内存。 而如果你仍想用这个已经创建的对象通过拷贝构造函数去创建一个新的对象,问题就出现了?什么问题呢?就是你问的问题(嘿嘿~~)。 如果你让新对象的指针成员直接赋值为已知对象的指针成员值,就意味着两个对象指向了同一块内存。 那样的话在析构对象时,因为有一个先后顺序之分,就会对同一块内存释放两次,就会导致系统崩溃!这就是所谓的浅拷贝。 那深拷贝呢?也就是解决了如上的麻烦。 其解决方案就是重新为新对象的指针成员开辟空间,然后把已知对象的指针所指内容拷贝过去,实现内容的完全一致。并且保证各自独立。 浅拷贝示例代码如下: 1 #include<iostream> 2 #include<assert.h> 3 #include<string.h> 4 using namespace std; 5 class String 6 { 7 public: 8 String(const char *str=NULL); 9 String(const String &other); 10 ~String(); 11 private: 12 char *m_data; 13 }; 14 15 String::String(const char *str) 16 { 17 if(str==NULL) 18 { 19 m_data=new char[1]; 20 assert(m_data!=NULL); 21 *m_data='