类和动态内存分配:静态类成员、复制构造函数、重载赋值运算符、类的静态成员函数、指向对象的指针、将定位new运算符用于对象、成员初始化列表及类内初始化
1.静态类成员 P349
静态类成员的特点:无论创建了多少对象,程序都只创建一个静态类变量的副本。即类的所有对象共享同一个静态成员。
不能在类声明中初始化静态成员变量;类的静态成员必须在类内声明,在类声明之外使用单独的语句来进行初始化,且在类外初始化时使用作用域运算符,但不使用关键字static。
但是类的静态const成员或枚举类型可以在类声明中初始化。P303
如:
class A { private: static int count ; // 类内声明 }; ... int A::count = 0 ; // 类外初始化,不必再加static关键字,注意,不是在类外使用 A::count = 0;初始化,而要加上类型,即 int A::count = 0;
但类的静态const成员或枚举类型可以在类声明中初始化:
class Bakery{ private: enum {Months = 12}; double costs[Months]; ... };
class Bakery{ private: static const int Months = 12; double costs[Months]; ... };
一篇解释如下:
https://blog.csdn.net/jiayi_yao/article/details/50998765
2.复制构造函数(拷贝构造函数)与类的默认的重载赋值运算符 P353
参考:
https://blog.csdn.net/hui2702/article/details/106089097
https://blog.csdn.net/xiaozhidian/article/details/114377907
复制构造函数的原型:
Class_name(const Class_name &);
如:
Student(const Student &);//复制构造函数(拷贝构造函数)
一)何时调用:
复制构造函数和默认的重载赋值运算符的最大区别即是赋值运算符没有新的对象生成,而拷贝构造函数会生成新的对象。
调用复制构造函数的场景:
1)对象通过另外一个对象进行初始化
student a("jack",2116112); student b = a;//调用copy构造函数,使用一个对象初始化另一个对象 student c;//c在这里使用默认构造函数初始化了 c = a;//调用重载赋值运算符函数,c已经被初始化过,所以这里属于赋值
要注意,使用默认构造函数初始化的对象也是被初始化的。
2)对象作为函数的参数,以值传递的方式传给函数。
3)当对象以值传递的方式从函数返回, 且接受返回值的对象没有初始化;
调用默认的重载的赋值运算符的场景:P356
1)对象以值传递方式从函数返回,且接受返回值的对象已经初始化过;
student a("jack",2116112); student c;//c在这里使用默认构造函数初始化了 c = a;//调用重载赋值运算符函数,c已经被初始化过,所以这里属于赋值
2)对象直接赋值给另一个对象,且接受值的对象已经初始化过
可见p389 T4
可以运行如下例子来辨析:
#include <iostream> #include <string> #include <cstring> using namespace std; class student { private: char name[50]; int number; public: student();//默认构造函数 student(const char *, int);//构造函数 student(const student &);//拷贝构造函数 student & operator=(student &); ~student(); }; int main() { student a("jack",100); student b; b = a;//这是赋值,不是舒适化 student c = a; student d = student("judy",200); // student c = student("mark",200); // a = c; // b = c; // a = c; } student::student() { } student::~student() { } student::student(const char * s, int n) { strcpy(name,s); number = n; cout<<"call constuctor."<<endl; } student::student(const student & s) { strcpy(name,s.name); number = s.number; cout<<"call copy consturctor."<<endl; } student & student::operator=(student & s) { strcpy(name,s.name); number = s.number; cout<<"call operator= function."<<endl; return s; }
二)默认复制构造函数和类的默认赋值重载运算符的功能:
默认的赋值构造函数逐个赋值非静态成员(成员赋值也称浅复制,只复制指针值 p353,p355),复制的是成员的值;如果类成员本身就是类对象,则将使用这个类的复制构造函数来复制成员对象。静态成员不受影响,因为它们属于整个类,而不是各个对象。
与默认的复制构造函数类似,默认的重载赋值运算符的实现也对成员进行逐个赋值;如果类成员本身就是类对象,则使用这个类定义的赋值运算符来复制该成员,但静态数据成员不受影响。
三)构造函数中使用new关键字的类,应: P375
1)若析构函数通过对指针类成员使用delete来释放内存,则每个构造函数都应该使用new来初始化指针,或将它设置为空指针nullptr(因为delete(无论是带[]或者不带[]的)都可以用于空指针。p364)
2)构造函数中要么使用new[],要么使用new,不能混用。如果构造函数使用new[],则析构函数使用delete[];如果构造函数使用new,则析构函数使用delete。
3)应定义一个复制构造函数,通过深度复制(即复制指针指向的数据,而不是只复制指针值 p355)将一个对象初始化为另一个对象。p364
该复制构造函数应该与下面类似:
String::String(const String & st) { num_strings++; len = st.len; str = new char [len + 1]; std::strcpy(str, st.str);//深拷贝,而不是使用str = st.str,这样会使str 和 st.str指向同一个字符串,在调用它们的析构函数时会释放同一个字符串 }
4)应定义一个重载赋值运算符,通过深度复制将一个对象复制给另一个对象。 p364
该重载的赋值运算符应该与下面类似:
String & String::operator=(const String & st) { if(this == &st) return this; delete [] str;//str是类的私有成员对象,用于存储指向字符串的指针 len = st.len; str = new char [len + 1]; std::strcpy(str, st.str);//深拷贝,而不是使用str = st.str,这样会使str 和 st.str指向同一个字符串,在调用它们的析构函数时会释放同一个字符串 return * this; }
三)中关于重新定义复制构造函数和重载赋值运算符的原因:p355
当类中包含了使用new初始化的指针成员时,若将一个类对象a赋值(使用复制构造函数或者重载的赋值运算符)给对象b时,会调用默认的复制构造函数或者重载的赋值运算符;此举会导致对象a、b指向同一个存放在new申请的动态内存中的字符串(数据),而当对象过期,调用a、b的析构函数时,两个析构函数会将该动态空间中的字符串(数据)释放两次,从而引发错误。
三)中关于delete的问题:p358
析构函数中若包含 delete [] str; 则构造函数中应使用 new []初始化指针,或使用空指针初始化指针;因为delete [] 和使用 new []初始化的指针和空指针 nullptr 初始化的指针都兼容。因此若析构函数中使用 delete [],则构造函数中应该使用
String::String() { len = 0; str = new char[1]; str[0] = '\0'; }
而不是
String::String() { len = 0; str = new char; str[0] = '\0'; }
3.静态类成员函数 p360
静态类成员函数的函数声明必须包含关键字static,若函数定义是独立的,则函数定义中不包含关键字static。
静态类成员函数不能通过对象调用(且静态类成员函数不能使用this指针,因为this指针指向调用类成员函数的对象);
如果静态成员函数是在类的公有部分声明的,则可以使用类名和作用域解析运算符来调用它;
因为静态成员函数不与特定的对象关联,因此静态成员函数只能使用静态数据成员。
//若String类有一个公有的静态成员函数HowMany() class String{ ... public: static int HowMany(){return num_strings;} }; ... int main() { ... int count = String::HowMany();//调用类的公有静态成员函数 ... }
4.指向对象的指针
1)使用new初始化对象时,若Class_name是类,value的类型为Type_name,则
Class_name * p = new Class_name(value);
将调用如下构造函数:
Class_name(Type_name &);
下列语句:
Class_name * ptr = new Class_name;
将调用默认构造函数。
5.再谈定位new运算符 p371
将定位new运算符用于对象
6.成员初始化列表 p378
参考:
https://blog.csdn.net/u012611878/article/details/79200544
若一个Queue类中有一个const数据成员
class Queue{ ... const int qsize; ... };
则下列代码不能正常运行:
Queue::Queue(int qs)//Queue的构造函数 { qsize = qs; ... }
因为const变量必须在创建时就对其初始化,而不能先声明再赋值。
而类在调用构造函数时的过程为:
调用构造函数时,对象将在构造函数括号中的代码执行之前被创建。因此,调用Queue类的构造函数将导致程序首先给Queue的成员变量(其中就包含了const变量qsize)分配内存;然后,程序流程进入到括号中,使用常规的赋值方式将值存储到了内存中。
因此,对于const数据成员,必须在执行到构造函数体之前,即创建对象时、给成员变量分配内存是对成员变量进行初始化。
C++提供了 成员初始化列表 来完成该工作。
假如Classy是一个类,而mem1、mem2和mem3都是这个类的数据成员,则类的构造函数可以使用如下的语法来初始化数据成员:
Classy::Classy(int n, int m) :mem1(n),mem2(0),mem3(n*m+2) { ... }
这些初始化工作是在对象创建时、给成员变量分配内存时完成的;此时还未执行构造函数体中的任何代码。
注意:
1)成员初始化列表只能用于构造函数;
2)必须用这种格式来初始化非静态const数据成员(static const数据成员可以在类内初始化 p303)
3)必须用这种格式来初始化引用数据成员(因为必须在声明引用变量时将其初始化,不能先声明,再赋值 p211)
7.类内初始化
c++11允许类内初始化
class Classy{ int mem1 = 10;const int mem2 = 20; ... };
类内初始化与在构造函数中使用成员初始化列表等价:
Classy::Classy():mem1(10), mem2(20){...}//与上述类内初始化等价