c++入门之详细探讨类的一些行为

之前我们讨论过类成员的组成,尤其是成员函数,我们知道了定义一个类的时候,我们往往定义了:构造函数,析构函数,其他函数,以及友元函数(友元函数不是必须的)。

同时,我们知道了这样一个事情:在定义一个对象的时候:构造函数会被调用;对象被销毁的时候:会调用析构函数。友元函数提供给我们访问成员的另一种方式。

但这其中,会产生更多的细节。通过一些具体的细节,我们来感受一些类中的行为:

类声明如下:

 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! ”其实看起来更像地址。