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常考):
- 初始化const成员
- 初始化引用成员
- 成员对象没有默认构造函数
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 | 禁止隐式转换 | 判断题:哪些写法合法,哪些非法 |
| 构造顺序 | 先基类,再成员,后自己 | 给出类关系,写出构造函数的输出顺序 |
记忆口诀
- 构造顺序:先基类后派生,先成员后自己
- 拷贝时机:三个地方要拷贝(传值、返回、显式构造)
- 初始化列表:常量引用必须用,效率考虑推荐用
另外,我觉得这篇文章讲解得特别好:《类和对象(超详细版)》作者:天上飞的粉红小猪
&spm=1001.2101.3001.5002&articleId=158351111&d=1&t=3&u=fa2917ca96684206bb51b3a61d4b60b5)
1078

被折叠的 条评论
为什么被折叠?



