之前我们讨论过类成员的组成,尤其是成员函数,我们知道了定义一个类的时候,我们往往定义了:构造函数,析构函数,其他函数,以及友元函数(友元函数不是必须的)。
同时,我们知道了这样一个事情:在定义一个对象的时候:构造函数会被调用;对象被销毁的时候:会调用析构函数。友元函数提供给我们访问成员的另一种方式。
但这其中,会产生更多的细节。通过一些具体的细节,我们来感受一些类中的行为:
类声明如下:
1 # include "iostream" 2 # ifndef STRNGBAD_H_ 3 # define STRNGBAD_H_ 4 class StringBad 5 { 6 private: 7 char *str; 8 int len; 9 static int num_strings; //= 0;//可见除了const 量之外,类内部成员是不能在内部赋初值的 10 public: 11 StringBad(const char * s); 12 StringBad(); 13 ~StringBad(); 14 15 friend std::ostream & operator<<(std::ostream & os, const StringBad & st); 16 }; 17 # endif
上述这个类声明中:
和通常一样:包含了构造函数,析构函数,友元函数;但这里需要强调的是 静态成员变量:num_strings。当我们使用StringBad类生成A,B,C....诸多类对象的时候,A.str,B.str,C.str....这些变量本质上地址是不同的。但是A.num_strings,B.num_strings,A.num_strings,地址是相同的。即:静态成员是属于类,而不是属于对象的。
我们看看这个类的定义:
1 # include "cstring" 2 # include "strngbad.h" 3 4 using std::cout; 5 6 int StringBad::num_strings = 0; 7 8 StringBad::StringBad(const char*s) 9 { 10 len = std::strlen(s); 11 str = new char[len + 1]; 12 std::strcpy(str, s); 13 num_strings++; 14 cout << num_strings << ": "" << str << "" object created "; 15 } 16 17 StringBad::StringBad() 18 { 19 len = 4; 20 str = new char[4]; 21 std::strcpy(str, "C++"); 22 num_strings++; 23 cout << num_strings << ": "" << str << "" default created "; 24 } 25 26 StringBad::~StringBad() 27 { 28 cout << """ << str << "" object created. "; 29 --num_strings; 30 cout << num_strings << "left "; 31 delete[] str; 32 } 33 34 std::ostream & operator<<(std::ostream & os, const StringBad & st) 35 { 36 os << st.str;//注意这里打印的是 str,如果不加str呢 37 return os; 38 }
关于这个类定义:我们注意这么几点:第6行进行了静态变量的初始化。注意,对num_strings 进行初始化的时候,StringBad::num_strings表明num_strings成员属于StringBad
问题:可以在类声明中进行初始化吗?
答:不可以,类声明只是说明需要为什么样的类型分配多少空间,但并不真正的分配空间,因此不能在声明中初始化(const除外)。
从这个类定义文件可以看出:无论是描述类成员函数,还是类成员变量.我们都要在前面描述其作用域!
new 和delete在动态内存分配中是十分重要的。但是要注意:在构造函数中使用了new[],在析构函数中必有delete[].
继续看调用文件:
1 # include "iostream" 2 using std::cout; 3 # include "strngbad.h" 4 5 void callme1(StringBad &); 6 void callme2(StringBad); 7 8 int main() 9 { 10 using std::endl; 11 { 12 cout << "Starting an inner block. "; 13 StringBad headline1("hello world!"); 14 StringBad headline2("learning forever!"); 15 StringBad sports("i love trvaling!"); 16 cout << "headline1" << headline1 << endl; 17 cout << "headline12"<< headline2 << endl; 18 cout << "sports" << sports << endl; 19 callme1(headline1); 20 cout << "headline1" << headline1 << endl; 21 callme2(headline2); 22 cout << "headline2" << headline2 << endl; 23 cout << "Initialize one object to another: "; 24 StringBad sailor = sports; 25 cout << "sailor: " << sailor << endl; 26 } 27 cout << "End of main() "; 28 system("pause"); 29 return 0; 30 } 31 32 void callme1(StringBad & rsb) 33 { 34 cout << "String passed by reference: "; 35 cout << " " << rsb << "" "; 36 } 37 38 void callme2(StringBad sb) 39 { 40 cout << "String passed by rvalue: "; 41 cout << " " << sb << "" "; 42 }
在此,给出第19行代码和第21行代码的一些细节:
第19行代码,传递参数的行为是引用传递,21行为:值传递。
再次强调:值传递 和 值返回 函数的两个本质特征:1. 进行值传递的时候,复制实参的副本,使得形参为实参的副本;2. 值返回的时候,复制返回值的副本(并销毁该变量(因为栈中的变量生存周期为函数调用期)),对副本进行后续操作。
因此第21行的代码:在进行按值传递的时候,首先创建一个对象的副本。但在创建对象副本的时候,首先会调用构造函数,但是这个构造函数是谁呢?是之前类定义中的构造函数吗?如果是,显然这个创建副本的形式应该为:()或者(cha*),但问题是:这里是吗???显然不是,那是什么呢???其实为:B=A,即将已知的对象A赋值给临时对象B,这就如同第24行的代码。那么这样的初始化方式,调用哪个构造函数呢???其实,是一种叫做赋值构造函数,专门应对这种赋值初始化的操作。很明显,之前的声明和定义中并没有赋值构造函数。那么这个赋值构造函数只能由编译器产生。
终于知道为何开始要求我们采用()或者{}的初始化方式,而不是"="的初始化方式。因为通常的我们可能并没有显示的定义自己的赋值构造函数,这个时候由编译器产生一个这样的函数,可能会存在一定的风险。但如果我们不可避免会采用到=或者值传递创建函数的形式,我们应该显示的定义自己的赋值构造函数,使得我们的程序风险更低。(实际上,通常的定义一个显示赋值构造函数更保险)
总结:按值传递和按值返回对象的时候,都将调用赋值构造函数!2构造函数中创建一个显示赋值构造函数通常更保险
我们在此再次声明:“hello World!”表示一个地址而不是字符串。当我们定义:p = “hello Word!”,假如我们定义:q = p,那么:是否是将字符串P复制了一遍,并存到了q的地址呢?并不是,而是:p,q都指向了“hello world!”,而这个时候,“hello world! ”其实看起来更像地址。