Skip to content

Chapter 7 Copy Constructor

1 References

  • 引用成员不能先声明后赋值,必须在构造函数初始化列表中完成绑定

示例

class X {
public:
    int& m_y;
    X(int& a);
};

X::X(int& a) : m_y(a) {}
  • 函数可以返回引用,但返回的引用必须指向仍然存在的对象,通常应返回非局部对象(全局对象、静态对象、调用方传入对象、容器元素等)
示例
#include <cassert>
const int SIZE = 32;
double myarray[SIZE];  // 全局数组,生命周期和整个程序一样

double& subscript(const int i) {
    return myarray[i];  // 返回全局数组第 i 个元素的引用
}

main() {
    // 1. 初始化数组
    for (int i = 0; i < SIZE; i++) {
        myarray[i] = i * 0.5;
    }

    // 2. 通过函数读取数组元素的值
    double value = subscript(12);

    // 3. 通过函数直接修改数组元素的值
    subscript(3) = 34.5;
}
禁止返回局部变量引用
double& bad_subscript(const int i) {
    double temp = myarray[i];
    return temp;  // error
}

局部变量在函数返回后生命周期结束,此时返回的引用会变成悬空引用(dangling reference)

2 Const

  • const 值传递通常没有意义,更常见的是 const 引用,如 Person(const string& name, int weight);
  • const 引用传递的优点是不修改实参、避免一次拷贝且 const 提供接口保护
示例:const 引用参数
void func(const int& y, int& z) {
    z = z * 5; // ok
    y += 8;    // error
}

临时值是 const

void func(int&);
func(i * 3); // warning / error

可以理解为:

const int _tmp_ = i * 3;
func(_tmp_); // 非 const 引用不能绑定到临时常量
  • 返回 const 值基本没意义
  • 返回 const 引用和 const 指针是否需要取决于是否希望调用方修改返回对象

3 Copy Constructor

  • 拷贝的本质:用已有对象创建新对象,常用于值传递参数、初始化新对象和函数按值返回
示例
void func(Currency p) {
    cout << "X = " << p.dollars();
}

Currency bucks(100, 0);
func(bucks); // bucks 被复制到 p
  • 拷贝操作是由拷贝构造函数来实现的,函数签名为 T::T(const T&);
  • 如果没有手动定义拷贝构造函数,C++ 编译器会自动生成默认拷贝构造函数
  • 默认拷贝构造函数的默认行为是进行逐成员拷贝,对数值类型、普通对象、对象数组都是安全可用的
  • 但对裸指针只是复制地址,可能导致共享同一块内存,引发数据冲突或崩溃问题

含裸指针的类
class Person {
public:
    // 构造函数:接收一个 C 风格字符串,用来初始化名字
    Person(const char *s);
    // 析构函数:用来释放名字占用的内存
    ~Person();
    void print();
    // ... accessor functions

private:
    char *name;  // 用原生 char* 而不是 std::string
};
  • C 风格字符串本质是字符数组,结尾有终止符 '\0'"C++" 在内存中实际占 4 个字符
<cstring> 常用函数
size_t strlen(const char* s);      // 长度,不含 '\0'
char* strcpy(char* dest, const char* src); // 拷贝字符串
  • 参数必须写成引用,若写成值传递,会再次触发拷贝构造,形成递归
char* 字符串成员的 Person 类的构造函数和析构函数实现
#include <cstring>
using namespace std;

Person::Person(const char* s) {
    name = new char[::strlen(s) + 1];
    ::strcpy(name, s);
}

Person::~Person() {
    delete[] name;
}
手动实现的拷贝构造(深拷贝)
Person::Person(const Person& w) {
    name = new char[::strlen(w.name) + 1];
    ::strcpy(name, w.name);
}
  • 拷贝构造函数可以跨对象边界访问 w.name(原对象的私有成员)
  • 拷贝构造函数的作用是初始化新对象的未初始化内存
What if the name was a string (and not a char*)?
#include <string>
class Person {
public:
    Person( const std::string& );
    ~Person();
    void print();
    // ... 其他访问器函数(getter/setter)...
private:
    std::string name;  // 嵌入式对象(组合关系:Person 对象包含一个 std::string 对象)
    // ...其他数据成员...
};
  • 在编译器自动生成的默认拷贝构造函数中,它会递归调用所有成员对象(以及基类)的拷贝构造函数,std::string 自动管理内存,无需手动分配 / 释放,析构时自动清理
When are copy ctors called?
During call by value
void roster( Person );      // declare function
Person child( "Ruby" );     // create object
roster( child );            // call function

During initialization
Person baby_a("Fred");
// these use the copy ctor
Person baby_b = baby_a; // not an assignment
Person baby_c(baby_a);  // not an assignment

During function return
Person captain() {
    Person player("George");
    return player;
}

Person who = captain();

  • 在不改变程序语义、保证安全的前提下,编译器可以自动优化掉不必要的拷贝操作
  • 程序设计时要兼容不做优化的编译器,不要依赖编译器的拷贝消除,同时要主动考虑性能优化,避免不必要的拷贝操作
示例
Person copy_func(Person p) {
    p.print();
    return p; // copy ctor called!
}

Person nocopy_func(char* who) {
    return Person(who); // no copy needed!
}

Construction vs Assignment

  • 每个对象只会被构造一次
  • 每个对象应该只被销毁一次
  • 一个已构造好的对象可以被赋值很多次
初始化不是赋值
Person a("Fred");
Person b = a; // 拷贝构造

Person c("Barney");
c = a;        // 赋值运算
  • 大多数情况下不用自己写拷贝构造
  • 涉及裸指针、文件句柄等资源管理时要显式定义
  • 若不希望对象可复制,可以把拷贝构造声明为 private(旧写法)或 Person(const Person&) = delete;(C++11)

4 Static

static 的两层含义

  • 静态存储期:只分配一次,程序期间一直存在
  • 内部链接:名字只在当前源文件中可见

Global static hidden in file

int g_global;
static int s_local;

void func() { /* ... */ }
static void hidden() { /* ... */ }
  • g_global 可通过 extern 在别处声明使用
  • s_local / hidden() 只在当前 .cpp 中可见

Static inside functions

void f() {
    static int num_calls = 0;
    num_calls++;
}
  • 值在整个程序运行期都保留,但初始化只发生一次

Static applied to objects

class X {
    X(int, int);
    ~X();
};

void f() {
    static X my_X(10, 20);
}
  • 当执行流第一次走到该定义处时构造(最多构造一次),程序结束时析构

Conditional construction

void f(int x) {
    if (x > 10) {
        static X my_X(x, x * 21);
    }
}
  • 只有第一次满足条件时才构造,一旦构造,后续会保留状态

Global Objects

#include "X.h"
static X global_x(12, 34);
static X global_x2(8, 16);
  • 它们会在 main() 之前构造,同一文件中按出现顺序构造,在 main() 结束或 exit() 调用时析构
  • 静态成员变量(Static member variables)
    • 属于整个类,而不是某个对象
    • 只初始化一次
    • 需要在 .cpp 中提供定义(定义处不再写 static
  • 静态成员函数(Static member functions)
    • 没有隐式接收者 this,也不能声明为 const
    • 只能访问静态成员变量和全局对象,不能参与动态绑定

使用方式

<class name>::<static member>
<object variable>.<static member>