一、动态内存和类
1、静态类成员
(1)静态类成员的特点
无论创建多少对象,程序都只创建一个静态类变量副本。也就是说,类的所有对象都共享同一个静态成员。
(2)初始化静态成员变量
1)不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存。对于静态成员,可以在类声明之外使用单独的语句来进行初始化,这是因为静态类成员是单独存储的,而不是对象的组成部分。请注意,初始化语句指出了类型,并使用了作用域运算符,但没有使用关键字static。
2)静态类成员初始化是在方法文件中,而不是在类声明文件中进行的。这是因为类声明位于头文件中,程序可能将头文件包含在其他文件中。如果在头文件中进行初始化,将出现多个初始化语句副本,从而引发错误。
3)对于不能在类声明中初始化静态成员的一种例外情况是,静态数据成员为const整数类型或枚举型。即,静态数据成员在类声明中声明,在包含类方法的文件中初始化。初始化使用作用域运算符来指出静态成员所属的类。但如果静态成员是const整数类型或枚举型,则可以在类声明中初始化。
Person.h
1 #include <iostream> 2 #include <string> 3 class Person{ 4 private: 5 static std::string race;//种族,静态类成员。除了const修饰的静态整数类型或枚举可以在类声明中进行初始化,其他的的静态成员的初始化都应该放在类方法文件中 6 int age; 7 std::string name; 8 public: 9 Person(std::string name_ = "无名氏", int age_ = 0); 10 ~Person(); 11 friend std::ostream & operator<<(std::ostream & os,const Person & per); 12 };
Person.cpp
1 #include "Person.h" 2 std::string Person:: race = "黄种人";//初始化静态成员,需要有作用域运算符 3 Person::Person(std::string name_, int age_){ 4 name = name_; 5 age = age_; 6 } 7 std::ostream& operator<<(std::ostream &os,const Person & per){ 8 os << "姓名:" << per.name << ",年龄:" << per.age << ",种族:" << per.race; 9 return os; 10 }
main.cpp
1 #include <iostream> 2 #include "Person.h" 3 4 using namespace std; 5 6 int main(int argc, const char * argv[]) { 7 Person per1{"小强",30}; 8 Person per2{"小黄",24}; 9 cout << per1 << " " << per2 << endl; 10 per1.changeRace("白种人"); 11 cout << per1 << " " << per2; 12 return 0; 13 } 14 15 输出结果: 16 姓名:小强,年龄:30,种族:黄种人 17 姓名:小黄,年龄:24,种族:黄种人 18 姓名:小强,年龄:30,种族:白种人 19 姓名:小黄,年龄:24,种族:白种人
2、特殊成员函数
删除对象可以释放对象本身占用的内存,但是并不能释放属于对象成员的指针指向的内存。在构造函数中使用new来分配内存时,必须在相应的析构函数中使用delete来释放内存。如果使用new[](包括中括号)类分配内存,则应使用delete[](包括中括号)来释放内存。
在设计类的时候,C++自动提供了下面的这些成员函数:
*默认构造函数,如果没有定义构造函数;
*默认析构函数,如果没有定义;
*复制构造函数,如果没有定义;
*赋值运算符,如果没有定义;
*地址运算符,如果没有定义;
隐式地址运算符返回调用对象的地址(即this指针的地址)。C++11提供了另外两个特殊成员函数:移动构造函数和移动赋值运算符。
(1)默认构造函数
如果没有提供任何构造函数,C++将创建默认构造函数,是一个不接受任何参数,也不执行任何参数的构造函数,创建出来的对象的值是不确定的;
如果定义了构造函数,C++将不会定义默认构造函数,这个时候需要自己提供默认构造函数。如果希望在创建对象时不显式地对他进行初始化,则必须显式地定义默认构造函数。这种构造函数没有任何参数,但可以用它来设置特定的值。
带参数的构造函数也可以是默认构造函数,只要所有的参数都有默认值。
一个类只能有一个默认构造函数。
(2)复制构造函数
复制构造函数用于将对象复制到新创建的对象中。也就是说,它用于初始化过程中(包括按值传递参数),而不是常规的赋值过程。类的复制构造函数原型通常如下:
Class_name(const Class_name &);
它接受一个指向类对象的常量引用作为参数。对于复制构造函数,需要知道两点:何时调用和有何功能。
1)何时调用复制构造函数
*新建一个对象并将其初始化为同类现有对象时,复制构造函数将被调用。这在很多情况下都可能发生,最常见的情况是将新对象显式地初始化为现有的对象。例如,假设motto是一个Person对象,则下面的4种声明都将调用复制构造函数:
Person metoo(motto);
Person ditto = motto;
Person also = Person(motto);
Person *pPer = new Person(motto);
*每当程序生成对象副本时,编译器都将使用复制构造函数。具体地说,当函数按值传递对象或函数返回对象,都将使用复制构造函数。记住,按值传递意味着创建原始变量的一个副本。编译器生成临时对象时,也将使用复制构造函数。
由于按值传递对象将调用复制构造函数,因此应该按引用传递对象。这样可以节省调用构造函数的时间以及存储对象的空间。
2)默认的复制构造函数的功能
*默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值。
*如果类成员本身也是类对象,则将使用这个类的复制构造函数来复制成员对象。静态成员不受影响,因为他属于整个类,而不是各个对象。
提示:如果类中包含静态数据成员,其值将在新对象被创建时发生变化,则应该提供一个显式复制构造函数来处理静态数据成员的值变化问题。
警告:如果类中包含使用new初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而不是指针,这被称为深度复制。复制的另一种形式(成员复制或浅复制)只是复制指针。浅复制只是浅浅地复制指针信息,而不会深入“挖掘”以复制指针引用的结构。
(3)赋值运算符
C++允许类对象赋值,这是通过自动为类重载赋值运算符实现的。这种运算符的原型如下:
Class_name & Class_name:: operator=(const Class_name &);
它接受并返回一个指向类对象的引用。
1)赋值运算符的功能以及何时使用它
*将已有的对象赋给另一个对象时,将使用重载的赋值运算符;
*初始化对象时,并不一定会使用赋值运算符。用已有对象初始化新建对象时,总会调用复制构造函数,而使用=运算符时也允许调用赋值运算符。在使用=以已有对象初始化新建对象的时候,可能只使用复制构造函数来初始化新建对象;也有可能使用复制构造函数创建一个临时对象,然后通过赋值运算符将临时对象的值复制到新对象中。
与复制构造函数相似,复制运算符的隐式实现也对成员进行逐个复制。如果成员本身就是类对象,则程序将使用为这个类定义的赋值运算符来复制该成员,但静态数据成员不受影响。
2)解决赋值的问题
对于由于默认赋值运算符不合适而导致的问题(常见情况是类成员为使用new初始化的指针),解决办法是提供赋值运算符(进行深度复制)定义。其实现与复制构造函数相似,但也有区别:
*由于目标对象可能引用了以前分配的数据,所以函数应使用delete来释放这些数据。
*函数应当避免将对象赋给自身;否则,给对象重新赋值前,释放内存操作可能删除对象的内容。
*函数返回一个指向调用对象的引用。
通过返回一个对象,函数可以像常规赋值操作那样,连续进行赋值。
请仔细阅读下面的例子:
String.h
1 #include <iostream> 2 class String{ 3 private: 4 char *str; 5 size_t len; 6 static int num_strings;//静态成员不能在类声明中声明的时候进行初始化;但是包含const的整数类型或枚举静态成员可以在类声明中初始化 7 public: 8 String(const char *); 9 String(const String &);//复制构造函数 10 String();//默认构造函数;如果没有定义构造函数,c++将提供默认构造函数;但是,如果定义了构造函数,就需要自己提供默认构造函数 11 ~String(); 12 friend std::ostream& operator<<(std::ostream & , const String &); 13 String & operator=(const String &);//赋值运算符,需要返回调用对象本身的引用 14 15 };
String.cpp
1 #include "String.h" 2 #include <ostream> 3 int String::num_strings = 0;//静态成员在方法文件中进行初始化,初始化的时候不带关键字static,但是要使用作用域运算符 4 String::String(const char * s){ 5 len = strlen(s); 6 num_strings ++; 7 str = new char[len + 1]; 8 strcpy(str, s); 9 std::cout << num_strings << ":创建了"" << str << ""对象,现有"<< num_strings << "条 "; 10 } 11 String::String(const String & s){//定义复制构造函数 12 len = s.len; 13 str = new char[len + 1]; 14 strcpy(str, s.str); 15 num_strings ++; 16 std::cout << num_strings << ": 创建了""<<str << ""对象,现有" << num_strings <<"条 "; 17 } 18 String::String(){ 19 len = 3; 20 str = new char[4]; 21 strcpy(str, "C++"); 22 num_strings ++; 23 std::cout << num_strings << ": 创建了默认对象"" << str << "",现有" << num_strings <<"条 "; 24 } 25 String & String:: operator=(const String &s){ 26 if (this == &s) {//先判断是否是将对象自身赋值给自己 27 return *this; 28 } 29 len = s.len; 30 delete [] str;//先释放对象指针成员之前的指向的内存,然后根据参数对象使用new重新分配内存 31 str = new char[len + 1]; 32 strcpy(str, s.str); 33 return *this;//赋值运算符应该返回调用对象的引用,这样就可以实现连续运算 34 } 35 std::ostream & operator<<(std::ostream & os, const String & s){ 36 os << s.str ; 37 return os; 38 } 39 String::~String(){ 40 std::cout << "删除了"" << str << ""对象,还剩"; 41 delete [] str; 42 --num_strings; 43 std::cout << num_strings << "条 "; 44 }
main.cpp
1 #include <iostream> 2 #include "String.h" 3 4 using namespace std; 5 6 void callme1(String &); 7 void callme2(String); 8 9 int main(int argc, const char * argv[]) { 10 String str1("小红"); 11 String str2{"小哈"}; 12 String str3{"牛逼哄哄"}; 13 cout << "str1:" << str1 << " str2:" << str2 << " str3:" << str3 << endl; 14 //cout << "按引用传递str1: "; 15 callme1(str1); 16 cout << "按值传递str2: "; 17 callme2(str2); 18 cout << "赋值: "; 19 String str4; 20 str4 = str2; 21 cout << "str4:" << str4 << endl; 22 return 0; 23 } 24 void callme1(String & str){ 25 cout << "按引用传递: "; 26 cout << " "" << str << "" "; 27 } 28 void callme2(String str){ 29 //cout << "按值传递: "; 30 cout << " "" << str << "" "; 31 } 32 33 34 输出结果: 35 1:创建了"小红"对象,现有1条 36 2:创建了"小哈"对象,现有2条 37 3:创建了"牛逼哄哄"对象,现有3条 38 str1:小红 39 str2:小哈 40 str3:牛逼哄哄 41 按引用传递: 42 "小红" 43 按值传递str2: 44 4: 创建了"小哈"对象,现有4条 45 "小哈" 46 删除了"小哈"对象,还剩3条 47 赋值: 48 4: 创建了默认对象"C++",现有4条 49 str4:小哈 50 删除了"小哈"对象,还剩3条 51 删除了"牛逼哄哄"对象,还剩2条 52 删除了"小哈"对象,还剩1条 53 删除了"小红"对象,还剩0条
1)、中括号运算符的重载说明:在C++中,两个中括号组成一个运算符——中括号运算符,可以使用operator[]()来重载该运算符。需要注意的是,像中括号这种形式的运算符,位于最前面的是第一个操作数,位于运算符中间的是第二个操作数。例如,在表达式city[0]中,city是第一个操作数,[]是运算符,0是第二个操作数。
2)、可以将成员函数声明为静态的(函数声明必须包含关键字static,但如果函数定义是独立的,则其中不能包含关键字static),这样做有两个重要的后果:
*首先,不能通过对象调用静态成员函数;实际上,静态成员函数甚至不能使用this指针。如果静态成员函数是在公有部分声明的,则可以使用类名和作用域运算符来调用它。
*其次,由于静态成员函数不与特定的对象相关联,因此只能使用静态数据成员。
同样,也可以使用静态成员函数设置类级标记,以控制某些类接口的行为。
二、在构造函数中使用new时应注意的事项
在使用new初始化对象的指针成员时必须特别小心。具体地说,应当这样做:
*如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete;
*new和delete必须相互兼容。new对应delete,new[]对应于delete[];
*如果有多个构造函数,则必须以相同的方式使用new,要么都带中括号,要么都不带。因为只有一个析构函数,所有的构造函数都必须与他兼容。然而,可以在一个构造函数中使用new初始化指针,而在另一个构造函数中将指针初始化为空(0或C++11中的nullptr),这是因为delete(无论是带中括号还是不带中括号)可以用于空指针。
*应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。复制构造函数应分配足够的空间来存储复制的数据,并复制数据,而不仅仅是数据的地址。另外,还应该更新所有受到影响的静态类成员。
*应定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象。具体的说,赋值运算符应完成这些操作:检查自我赋值的情况,释放成员指针以前指向的内存,复制数据而不仅仅是数据的地址,并返回一个指向调用对象的引用。
三、有关返回对象的说明
当成员函数或独立的函数返回对象时,有几种返回方式可供选择。可以返回指向对象的引用、指向对象的const引用或const对象。
1、返回指向const对象的引用
使用const引用的常见原因是旨在提高效率,但对于何时可以采用这种方式存在一些限制。如果函数返回(通过调用对象的方法或将对象作为参数)传递给他的对象,可以通过返回引用来提高效率。
有三点需要说明:
*首先,返回对象将调用复制构造函数,而返回引用不会;
*其次,引用指向的对象应该在调用函数执行时存在;
*第三,返回类型必须与函数类型相匹配。
2、返回指向非const对象的引用
两种常见的返回指向非const对象的引用的情形是,重载赋值运算符以及重载与cout一起使用的<<运算符。前者这样做是旨在提高效率,而后者是必须这样做。
3、返回对象
如果被返回的对象是被调用函数中的局部变量,则不应按引用返回它,因为在被调用函数执行完毕时,局部对象将调用析构函数。因此当控制权回到调用函数时,引用指向的对象将不再存在。在这种情况下,应返回对象而不是引用;同时,存在调用复制构造函数来创建被返回的对象的开销,然而这是无法避免的。通常,被重载的算术运算符属于这一类。
4、返回const对象
假设一个只有int类型的数据成员lenth的类Vector存在Vector::operator+()的定义如下:
Vector Vector::operator+(const Vector &v){
int len = lenth + v.lenth;
Vector v2 = len;
return v2;
}
它旨在能够以下面这样的方式使用它:
net = force1 + force2;
然而,这种定义也允许这样使用它:
force1 + force2 = net;
cout << (force1 + force2 = net).lenth() << endl;
像下面的语句存在的错误在于:
*首先,没有编写这种语句的合理理由;
*其次,这种代码之所以可行,是因为复制构造函数创建了一个临时对象来表示返回值。因此,net被赋给了临时对象。
*第三,使用完临时对象后,将把他丢弃。
避免这种错误的一种简单的解决方案:将返回值类型声明为const。
总之,如果方法或函数要返回局部对象,则应返回对象,而不是指向对象的引用。在这种情况下,将使用复制构造函数来生成返回对象。如果方法或函数要返回一个没有公有复制构造函数的类的对象,它必须返回一个指向这种对象的引用。最后,有些方法或函数(如重载的赋值运算符)可以返回对象,也可以返回指向对象的引用,在这种情况下,应首选引用,因为其效率更高。
四、使用指向对象的指针
1 #include <iostream> 2 #include "String.h" 3 #include <cstdlib> 4 #include <ctime> 5 6 const int ArSize = 10; 7 const int MaxLen = 81; 8 9 using namespace std; 10 11 int main(int argc, const char * argv[]) { 12 String name; 13 cout << "Hi,what's your name? "; 14 cin >> name; 15 16 cout << name << ",please enter up to" << ArSize << " short saying <empty line to quit>: "; 17 String saying[ArSize]; 18 char temp[MaxLen]; 19 int i; 20 for (i = 0; i < ArSize; i++){ 21 cout << i + 1 << ":"; 22 cin.get(temp,MaxLen); 23 while (cin && cin.get() != ' ') { 24 continue; 25 } 26 if (!cin || temp[0] == '