成员函数

类声明时通常将短小的成员函数作为内联函数,以此来减少函数调用所带来的开销

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Class Stock
{
private:
std::string company;
long shares;
double share_val;
double total_val;
void set_tot(total_val = shares * share_val); // 内联函数
public:
void acquire(const std::string &co, long n, double pr);
void buy(long num, double price);
void sell(long num, double price);
void update(double price);
void show();
}

也可以在类声明外定义使其成为内联函数,只需显示声明 inline

1
2
3
4
5
6
7
8
9
10
class Stock
{
private:
void set_tot();
}

inline void Stock::set_tot()
{
total_val = shares * share_val;
}

注意,如果在类外定义 inline 函数,则必须将类定义和成员函数的定义都放在同一个头文件中(或者写在同一个源文件中),另外,是否真正内敛取决于编译器的判断

静态成员变量

假设类有个静态成员变量

1
static int num_strings;

可以在类方法文件中将其初始化

1
int StringBad::num_strings = 0;

但不能在类声明文件中进行,因为多个文件导入该头文件,会导致多次初始化

下面举个静态类成员作用的例子

1
2
3
4
5
6
7
class Bakery
{
private:
const int Months = 12; // declare a constant? FAILS
double costs[Months];
...
}

上面代码的做法是不可行的,因为在创建对象前,没有为 Months 变量分配空间

因此可以用了两种方式解决,第一种就是使用静态成员,因为静态存储区域在代码运行时就会被分配

1
2
3
4
5
6
7
class Bakery
{
private:
static const int Months = 12;
double costs[Months];
...
}

第二种是使用在类中声明一个枚举来创建符号常量

1
2
3
4
5
6
7
class Bakery
{
private:
enum {Months = 12};
double costs[Months];
...
}

静态成员函数

  • 静态成员函数是类级别的,而不是对象级别的,所以使用类名来进行调用,不能使用 this 指针
  • 只能访问静态成员变量,不能访问非静态成员变量
  • 可以修改静态成员变量

构造函数

类构造函数专门用于构造新对象、将值赋给它们的数据类型

为了避免构造函数的形参和类成员名称相同,一种常见的做法是在类成员名中使用 m_ 前缀,另一种常见的做法是使用后缀 _

1
2
3
4
5
6
class Stock
{
private:
string m_company;
long m_shares;
}

拷贝构造函数

1
StringBad sailor = sprots; // sports 也是 StringBad 类对象

这时等效于

1
StringBad sailor = StringBad(sprots);

当使用一个对象来初始化另一个对象时,编译器会自动生成 拷贝构造函数

1
StringBad(const StringBad &);

注意,拷贝构造函数中的传递为引用传递,因为当对象作为实参传递时,就需要调用拷贝构造函数,若为值传递,拷贝构造函数又会拷贝实参,为了拷贝实参有需要调用拷贝构造函数,这就造成了死循环,所以需要使用引用传递。

析构函数

用构造函数创建对象后,程序负责跟踪该对象,直到其过期为止。对象过期时,程序将自动调用一个特殊的成员函数,也就是析构函数,用于完成清理工作,如果构造函数使用 new 来分配内存,那么析构函数就用 delete 来释放内存

和构造函数一样,析构函数没有返回值和声明类型,但是析构函数没有参数

1
2
3
4
Stock::~Stock()
{
cout<< “bye” << endl;
}

析构函数一般不用写,但是如果构造函数使用了 new,则必须提供使用 delete 的析构函数

拷贝赋值运算符

前面提到 StringBad sailor = sprots; 其实是等效于 StringBad sailor = StringBad(sprots); 而不是调用赋值运算符

也就是说复制运算符用于创建新的对象,而拷贝赋值运算符是用于已存在的对象之间的赋值,所以重载赋值运算符的时候需要先删除目标对象已分配的动态内存空间

1
2
3
4
5
6
7
8
9
10
11
12
StringBad & StringBad::operator=(const StringBad & st)
{
if (this == &st) // object assigned to itself
return *this;

delete [] str; // free old string
len = st.len;
str = new char [len + 1]; // get space for new string
std::strcpy (str, st.str); // copy the string

return *this; // return reference to invoking object”
}

default 和 delete

当类中没有显示定义构造函数、析构函数等情况下,编译器将会自己生成合成构造函数、合成析构函数等(或者说是默认构造函数,默认析构函数),此时如果需要合成版本的函数,则需要使用 default 显示生成

1
2
3
4
5
6
7
class Sales_data {
public:
Sales_data() = default;
Sales_data(const Sales_data&) = default;
Sales_data& operator=(const Sales_data &) = default;
~Sales_data() = default;
}

当有些类不需要某些操作时,可以使用 delete 来阻止(删除)这些操作,比如 iostream 组织了拷贝,以免多个对象写入或读取相同的 I缓冲

1
2
3
4
5
6
struct NoCopy {
NoCopy() = default;
NoCopy(const NoCopy&) = delete; // 阻止拷贝
NoCopy &operator=(const NoCopy&) = delete; // 阻止赋值
~NoCopy() = default;
}

析构函数不能删除

如果一个类有数据成员不能默认构、拷贝、复制或销毁,则对应的成员函数将被定义为删除的

在发布新标准前,是通过定义在 private 中来进行 delete 操作 ## 三/五法则

如上所述,有三个基本操作可以控制类的拷贝操作:拷贝构造函数、拷贝赋值运算符和析构函数。在新标准下,一个类还可以定义一个移动构造函数和一个移动赋值运算符

  • 需要析构函数的类也需要拷贝和赋值操作
  • 需要拷贝操作的类也需要赋值操作,反之亦然
1
2
3
4
5
6
class HasPtr {
public:
HasPtr(const std::string &s = std::string()):
ps(new std::string(s), i(0)) { }
~HasPtr() { delete ps; }
}

上述代码错误,应该还需要定义一个拷贝构造函数和一个拷贝赋值函数,因为构造函数中分配了动态内存,内存将在对象销毁时被释放,如果使用合成的拷贝构造函数和拷贝赋值函数,这些函数则简单拷贝指针成员,就会有多个对象指向相同的内存,最后对象销毁时,会多次调用 delete,这显然是错误的。

  • 析构函数不能是删除的成员(不能定义该类型的变量,可以动态分配,但无法释放)
1
2
3
4
5
6
7
8
struct NoDtor {
NoDtor() = default; // 使用合成默认构造函数
~NoDtor() = delete; // 不能销毁 NoDtor 类型的对象
}

NoDtor nd; // 错误,析构函数是删除的
NoDtor *p = new NoDtor(); // 正确,但不能 delete 释放
delete p; // 错误,析构函数是删除的
  • 合成的拷贝控制成员可能是删除的(如果类有一个数据成员不能默认构造、拷贝、复制或销毁,则对应的成员函数将被定义为删除的)

移动对象

新标准的一个特性是可以移动而非拷贝对象的能力,在某些情况下,对象拷贝后就立即被销毁,在这种情况下,移动而非拷贝对象会大幅度提升性能,详见 对象移动