12.1动态内存和类
12.1.1一些分析
#include <iostream>
#ifndef STRNGBAD_H_
#define STRNGBAD_H_
class StringBad
{
private:
char* str;
int len;
static int num_strings;
public:
StringBad(const char* s);
StringBad();
~StringBad();
friend std::ostream& operator<<(std::ostream& os, const StringBad& st);
};
#endif // !STRNGBAD_H_
代码分析:
- 使用char指针而不是char数组来表示姓名,意味着类声明没有为字符串本身分配存储空间,而是在构造函数中使用new为字符串分配空间。
- num_strings成员为静态存储类。静态成员:无论创建多少对象,程序都只创建一个静态类变量副本,所有对象共享一个num_strings成员。
- 静态类成员类内声明,类外初始化。
#include <cstring>
#include "strngbad.h"
using std::cout;int StringBad::num_strings = 0;
StringBad::StringBad(const char* s)
{
len = std::strlen(s);
str = new char[len + 1];
std::strcpy(str, s);
num_strings++;
cout << num_strings << ": \"" << str << "\" objected created\n";
}
StringBad::StringBad()
{
len = 4;
str = new char[4];
std::strcpy(str, "C++");
num_strings++;
cout << num_strings << ": \"" << str << "\" default objected created\n";
}
StringBad::~StringBad()
{
cout << "\"" << str << "\" object deleted, ";
--num_strings;
cout << num_strings << " left\n";
delete[] str;
}
std::ostream& operator<<(std::ostream& os, const StringBad& st)
{
os << st.str;
return os;
}
- int StringBad::num_strings = 0;静态成员在类外声明。初始化是在方法文件中,而不是类声明文件中。因为类声明位于头文件中的话,其他文件都包含头文件,这会造成出现多个初始化语句副本,引发错误。
- 但如果静态成员是const整数类型(static const int month ....)可以在类声明中初始化,static const int month = 12;枚举型也可以在类声明中初始化。
- 在构造函数中使用new分配内存,必须在相应的析构函数中使用delete。如果使用new[]分配,应使用delete[]来释放。
- StringBad sailor = sports;等效于StringBad sailor = StringBad(sports);StringBad(sports)的构造函数原型为StringBad(const StringBad &);当使用一个对象初始化另一个对象时,编译器自动生成上述复制构造函数。
12.1.2特殊成员函数
C++自动提供以下成员函数:
默认构造函数,如果没有定义构造函数
默认析构函数,如果没有定义析构函数
复制构造函数,如果没有定义复制构造函数
赋值运算符,如果没有定义......
地址运算符,如果没有定义......(this指针)
1.默认构造函数
- 如果没有提供任何构造函数,C++将创建默认构造函数。如果定义了构造函数,C++ 将不会定义默认构造函数。
- 带参数的构造函数也可能是默认构造函数,只要所有参数都有默认值。Klunk(int n = 0){Klunk_ct = n;}但只能有一个默认构造函数。
2.复制构造函数
- 函数原型:Class_name(const Class_name &)参数是一个指向类对象的常量引用。
- 何时调用?新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。
StringBad ditto(motto);
StringBad metoo = motto;
StringBad also = StringBad(motto);
StringBad * pStringBad = new StringBad(motto);
当函数按值传递或者返回对象或者编译器生成临时对象时都将调用复制构造函数。
(按值传递将调用复制构造函数,所以应该按引用传递对象,这样可以节省调用构造函数的时间以及存储新对象的空间)。
3.默认的复制构造函数功能:逐个复制非静态成员的值(浅复制)
浅拷贝在析构时出现问题。解决方式:深拷贝。
复制构造函数应当复制字符串并将副本的地址赋给str成员,而不是仅仅复制字符串地址。
(我觉得这里出问题是因为str是字符串指针,如果是字符串,那无所谓浅拷贝深拷贝。“核心编程笔记”例子也是,是指针,所以得自己写拷贝构造函数)
4.警告:如果类中包含了使用new初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而不是指针。
3.赋值运算符
StringBad metoo = knot; //编译器处理时可能分为:使用可拷贝构造函数创建一个临时对象,然后通过赋值将临时对象赋值到新对象中。
书中由于默认赋值运算符不合适,所以提供赋值运算符定义:
String& String::operator=(const String& st)
{
if (this == &st)
return *this;
delete[] str;
len = st.len;
str = new char[len + 1];
strcpy_s(str,len+1, st.str);
return *this;
}
12.2
12.2.1静态类成员函数
注意:
1.不能通过对象调用静态成员函数;实际上,静态成员函数甚至不能使用this指针。如果静态成员函数是在公有部分声明的,则可以使用类名和作用域解析运算符来调用它。int count = String::Howmany;
2.静态成员函数不与特定对象关联,因此只能使用静态数据成员。
12.3在构造函数中使用new的注意事项
-
如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete
-
new和delete必须相互兼容,new对应于delete,new[]对应于delete[]
-
多个构造函数,相同的方式使用new ,要么都在中括号,要么都不带。因为只有一个析构函数,所有构造函数都必须与他兼容。
-
应该定义一个拷贝构造函数,通过深拷贝将一个对象初始化为另一个对象。
-
应当定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象。
12.4有关返回对象的说明
三种方式:
- 返回指向对象的引用
- 指向对象的const引用
- const对象
- 返回对象
12.4.1返回指向对象(非const对象)的引用
两种常见情形:
- 重载赋值运算符。(提高效率)
- 重载与cout一起使用的<<运算符。(必须)
12.4.2返回指向const对象的引用
使用const引用的常见原因是旨在提高效率。
- 返回对象将调用拷贝构造函数,而返回引用不会。因此,第二种效率更高。
- 引用指向的对象应该在调用函数执行时存在。(不能返回局部变量的引用)。
- v1和v2都被声明为const引用,因此返回的类型必须为const,这样才匹配。
12.4.3返回const对象
防止返回对象的误用和滥用。
12.4.4返回对象
如果被返回的对象是被调用函数中的局部变量,则不应该按引用的方式返回它,可以考虑直接返回对象。
总之:
- 如果方法或函数是返回局部对象,应该返回对象,而不是引用。
- 如果方法或者函数要返回一个没有公有拷贝构造函数的类(如ostream类)的对象,他必须返回一个指向这种对象的引用。
- 当可以返回对象也可以返回引用时,首选引用,因为效率高。
12.5
12.5.1new和delete小总结
- 如果对象是动态变量,则当执行完定义该对象的程序块时,将调用该对象的析构函数。
- 如果对象是静态变量(外部、静态、静态外部、或来自名称空间),在程序结束时将调用对象的析构函数。
- 如果对象是用new创建的,仅当使用delete删除对象时,其析构函数才会被调用。
12.5.2指针和对象小结
使用对象指针时,注意: