类的构造函数(GESP六级重点版)

1. 构造函数的基本概念

构造函数是类的特殊成员函数,在创建对象时自动调用,用于初始化对象。

基本特征

  • 函数名与类名相同
  • 没有返回值类型(连void都不写)
  • 可以重载(多个构造函数)
  • 自动调用,不能手动调用
class Student {
private:
    string name;
    int age;
    
public:
    // 构造函数:与类同名,无返回值
    Student() {
        name = "未知";
        age = 0;
    }
};

解释:这段代码定义了一个Student类,其中的Student()就是构造函数。当我们创建Student对象时(如Student s;),这个函数会自动执行,将name设置为"未知",age设置为0。构造函数的特点是:函数名与类名完全相同,没有返回值类型(不能写void),而且不能手动调用(如s.Student()是错误的)。


2. 构造函数的分类

2.1 默认构造函数

定义:不带参数的构造函数,或者所有参数都有默认值的构造函数。

class Test {
public:
    // 默认构造函数(无参数)
    Test() {
        cout << "默认构造" << endl;
    }
};

// 另一种形式:所有参数都有默认值
class Test2 {
public:
    Test2(int x = 0, string s = "") {  // 这也是默认构造函数
        // 初始化...
    }
};

int main() {
    Test t1;        // 调用默认构造函数
    Test2 t2;       // 调用带默认参数的构造函数
}

解释:默认构造函数指的是可以无参调用的构造函数。上面的Test类的构造函数没有参数,所以是默认构造函数。Test2类的构造函数虽然有参数,但每个参数都有默认值,所以调用时可以不用传参(如Test2 t2;),因此它也是默认构造函数。注意:一个类只能有一个默认构造函数,如果同时定义Test()Test(int=0),编译器会报错(二义性)。

GESP考点:如果类中没有定义任何构造函数,编译器会自动生成一个默认构造函数。这个自动生成的构造函数会调用成员对象的默认构造函数,但对基本类型(int、double等)不做初始化(值不确定)。

2.2 带参数的构造函数

用于提供不同的初始化方式。

class Rectangle {
private:
    int width, height;
    
public:
    // 带参数的构造函数
    Rectangle(int w, int h) {
        width = w;
        height = h;
    }
};

int main() {
    Rectangle r1(10, 20);    // 正确:调用带参构造函数
    // Rectangle r2;          // 错误!没有默认构造函数了
}

解释:当我们定义了带参数的构造函数后,编译器就不会再自动生成默认构造函数。所以上面代码中,创建r1时必须传入参数,而Rectangle r2;这种无参创建方式会报错,因为类中已经没有无参构造函数可调用了。如果想同时支持无参和有参创建,需要自己提供两个构造函数(函数重载)。


3. 初始化列表

初始化列表是在构造函数参数列表后面,用冒号开头的成员初始化方式。

class Point {
private:
    int x;
    int y;
    const int id;      // 常量成员
    int& ref;          // 引用成员
    
public:
    // 构造函数 + 初始化列表
    Point(int a, int b, int i, int& r) 
        : x(a), y(b), id(i), ref(r)   // 初始化列表
    {
        // 构造函数体可以为空
    }
};

解释:初始化列表是在构造函数体执行之前初始化成员的地方。上面的代码中,: x(a), y(b), id(i), ref(r)就是初始化列表,它把参数a的值赋给成员x,b给y,i给id,r给ref。注意初始化列表的执行顺序由成员在类中声明的顺序决定,而不是初始化列表中的顺序。

必须使用初始化列表的情况(GESP常考):

  1. 初始化const成员
  2. 初始化引用成员
  3. 成员对象没有默认构造函数
class ConstRef {
private:
    const int a;        // 常量
    int& b;             // 引用
    string& s;          // 引用
    
public:
    // 必须使用初始化列表
    ConstRef(int x, int& y, string& str) 
        : a(x), b(y), s(str)    // 正确
    {
        // a = x;  错误:常量不能赋值
        // b = y;  错误:引用必须初始化
    }
};

解释:这段代码展示了为什么const和引用成员必须用初始化列表。在构造函数体内,a = x这样的赋值语句是不允许的,因为const变量一旦创建就不能修改。引用b必须在定义时就绑定到一个变量,不能先定义再赋值。初始化列表正好是在对象创建时(进入函数体之前)进行初始化,所以能满足这些特殊成员的要求。


4. 拷贝构造函数

定义:用同类型的对象来初始化新对象。

语法形式

class ClassName {
public:
    // 拷贝构造函数:参数为同类型的const引用
    ClassName(const ClassName& other) {
        // 复制other中的数据到当前对象
    }
};

解释:拷贝构造函数的参数必须是同类型的引用,通常加const修饰。为什么必须是引用?如果写成ClassName(ClassName other),那么传参时又要调用拷贝构造函数,就会造成无限递归。所以标准规定拷贝构造函数的参数必须是引用类型。

调用时机(GESP常考)

class Student {
private:
    string name;
    int score;
    
public:
    Student(string n, int s) : name(n), score(s) {}
    
    // 拷贝构造函数
    Student(const Student& other) {
        name = other.name;
        score = other.score;
        cout << "拷贝构造被调用" << endl;
    }
    
    void show() {
        cout << name << ": " << score << endl;
    }
};

// 函数传参(值传递)
void func(Student s) {    // 调用拷贝构造函数
    s.show();
}

// 函数返回值(返回对象)
Student createStudent() {
    Student temp("张三", 90);
    return temp;          // 可能调用拷贝构造
}

int main() {
    Student s1("李四", 85);
    
    Student s2(s1);        // 情况1:显式拷贝构造
    Student s3 = s1;       // 情况2:隐式拷贝构造(不是赋值!)
    
    func(s1);              // 情况3:传值调用
    
    // 下面不是拷贝构造
    Student s4("王五", 95);  // 普通构造
    s4 = s1;                 // 赋值运算符,不是拷贝构造
}

解释:这段代码展示了拷贝构造函数的三种调用情况。特别要注意Student s3 = s1;这行,虽然用了等号,但这是初始化,不是赋值,所以调用的是拷贝构造函数。而s4 = s1;是赋值操作,调用的是赋值运算符函数(operator=)。区分"初始化"和"赋值"是GESP常考的点:初始化是在创建对象的同时给值,赋值是对象已经存在后修改值。

默认拷贝构造的问题(GESP考点)

编译器提供的默认拷贝构造是浅拷贝(逐字节复制)。

class String {
private:
    char* str;      // 指针成员
    
public:
    String(const char* s) {
        str = new char[100];
        strcpy(str, s);
    }
    
    // 如果不自定义拷贝构造,默认浅拷贝会导致问题
    ~String() {
        delete[] str;   // 两个对象会重复释放同一内存
    }
};

// 正确的深拷贝实现
class SafeString {
private:
    char* str;
    
public:
    SafeString(const char* s = "") {
        str = new char[strlen(s) + 1];
        strcpy(str, s);
    }
    
    // 深拷贝构造函数
    SafeString(const SafeString& other) {
        str = new char[strlen(other.str) + 1];  // 分配新内存
        strcpy(str, other.str);                   // 复制内容
    }
    
    ~SafeString() {
        delete[] str;
    }
};

解释:默认拷贝构造函数只是简单复制每个成员的值。对于指针成员,它复制的是指针本身的值(内存地址),而不是指针指向的内容。这导致两个对象的str指向同一块内存。当两个对象销毁时,都会调用析构函数释放同一块内存,造成"重复释放"错误。深拷贝则是先分配新的内存,再复制内容,让每个对象拥有独立的内存空间。


5. explicit关键字

作用:禁止构造函数的隐式类型转换。

class Number {
private:
    int value;
    
public:
    // 普通构造函数(允许隐式转换)
    Number(int n) : value(n) {}
    
    // explicit构造函数(禁止隐式转换)
    explicit Number(double d) : value((int)d) {}
};

void printNumber(Number n) {
    // 函数体
}

int main() {
    // 普通构造函数:允许隐式转换
    Number n1 = 100;      // 隐式转换:int → Number
    
    // explicit构造函数:禁止隐式转换
    // Number n2 = 3.14;   // 错误!不能隐式转换
    
    Number n2(3.14);       // 正确:显式调用
    Number n3 = (Number)3.14;  // 正确:强制类型转换
    
    // 函数调用时的隐式转换
    printNumber(200);      // 正确:隐式转换 int → Number
    // printNumber(3.14);   // 错误:explicit禁止隐式转换
}

解释:explicit关键字用来防止构造函数被用于隐式类型转换。Number n1 = 100;这行实际上发生了隐式转换:编译器把100构造成一个临时Number对象,然后用这个对象初始化n1(优化后可能直接构造)。这种隐式转换有时会带来意想不到的结果,所以可以用explicit禁止。加上explicit后,必须显式调用构造函数(Number(3.14))或使用强制类型转换才能创建对象。


6. GESP六级常见考题形式

题型1:构造函数调用顺序

class A {
public:
    A() { cout << "A"; }
};

class B {
private:
    A a;    // 成员对象
public:
    B() { cout << "B"; }
};

int main() {
    B b;    // 输出什么? 答:AB
    // 先构造成员对象a,再执行B的构造函数体
}

解释:构造函数的执行顺序是:先构造成员对象(按声明顺序),再执行自己的构造函数体。所以创建B对象时,先构造成员a(输出"A"),然后执行B的构造函数体(输出"B")。这个顺序是固定的,不管初始化列表怎么写。

题型2:拷贝构造的调用次数

class Test {
public:
    Test() { cout << "构造 "; }
    Test(const Test&) { cout << "拷贝 "; }
};

Test func(Test t) {    // 传值:拷贝
    return t;          // 返回值:拷贝
}

int main() {
    Test t1;           // 构造
    Test t2 = func(t1); // 调用func:传值拷贝1次,返回拷贝1次
    // 实际输出顺序取决于编译器的返回值优化(RVO)
}

解释:理论上,func(t1)调用时,传参会调用一次拷贝构造(把t1复制给参数t),返回时又会调用一次拷贝构造(把t复制给返回值)。所以应该输出"构造 拷贝 拷贝"。但现代编译器会进行返回值优化(RVO),可能减少拷贝次数,这是GESP考试中需要注意的点:理论次数和实际次数可能不同。

题型3:初始化列表的必要性

class MyClass {
private:
    const int id;          // 常量成员
    int& ref;              // 引用成员
    
public:
    // 必须使用初始化列表
    MyClass(int x, int& r) : id(x), ref(r) {
        // 正确
    }
    
    /*
    MyClass(int x, int& r) {
        id = x;    // 错误!常量不能赋值
        ref = r;   // 错误!引用必须初始化
    }
    */
};

解释:注释部分展示了如果不使用初始化列表会怎样。const成员必须在创建时初始化,不能在构造函数体内赋值(因为赋值是修改,而const变量不能修改)。引用成员必须在定义时绑定到一个变量,也不能在函数体内赋值(赋值是改变引用绑定的对象,而不是初始化引用)。所以这两种成员必须用初始化列表。

题型4:深拷贝与浅拷贝

class Array {
private:
    int* data;
    int size;
    
public:
    Array(int n) : size(n) {
        data = new int[size];
    }
    
    // 必须实现深拷贝
    Array(const Array& other) : size(other.size) {
        data = new int[size];              // 新内存
        for(int i = 0; i < size; i++) {
            data[i] = other.data[i];        // 复制数据
        }
    }
    
    ~Array() {
        delete[] data;
    }
};

解释:这个Array类管理着动态分配的数组内存。如果不自定义拷贝构造函数,默认的浅拷贝会让两个对象的data指向同一块内存。当其中一个对象销毁时,那块内存被释放,另一个对象的data就变成了悬空指针。深拷贝通过分配新内存并复制数据,让每个对象拥有独立的内存空间,避免了这个问题。


7. GESP六级重点总结

概念关键点常考形式
默认构造函数无参/全缺省参数判断编译器何时自动生成,哪些情况不生成
初始化列表const/引用成员必须用选择题:哪些成员必须用初始化列表
拷贝构造浅拷贝问题、调用时机找出程序中的拷贝构造调用,判断深浅拷贝
explicit禁止隐式转换判断题:哪些写法合法,哪些非法
构造顺序先基类,再成员,后自己给出类关系,写出构造函数的输出顺序

记忆口诀

  • 构造顺序:先基类后派生,先成员后自己
  • 拷贝时机:三个地方要拷贝(传值、返回、显式构造)
  • 初始化列表:常量引用必须用,效率考虑推荐用

另外,我觉得这篇文章讲解得特别好:《类和对象(超详细版)》作者:天上飞的粉红小猪

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值