11、为什么构造函数可以重载,析构函数不可以?
构造函数用于对象的创建,不同场景可能需要不同的初始化参数。
class MyClass {
public:
MyClass() { /* 默认初始化 */ } // 无参构造函数
MyClass(int x) { /* 用x初始化 */ } // 单参数构造函数
MyClass(int x, double y) { /* 复杂初始化 */ } // 双参数构造函数
};
语法上:构造函数与类名相同且无返回值,但允许参数列表不同。
C++标准规定析构函数必须无参数且唯一。
析构函数在对象销毁时由编译器自动调用,且无参数。若允许重载,编译器无法确定应调用哪个版本。析构函数的核心职责是释放资源(如内存、文件句柄等),通常需要统一的清理逻辑。若需差异化处理,应通过成员变量状态控制,而非重载
class FileHandler {
private:
bool isOpen = false;
public:
~FileHandler() {
if (isOpen) fclose(filePtr); // 根据状态统一处理
}
};
12、 构造函数可以是虚函数吗?为什么?
对象尚未完全构造
虚函数的调用通过对象的虚指针(vptr)和虚表(vtable)实现动态绑定。
在构造函数执行时,对象的虚指针尚未初始化(仅在基类构造函数完成后才指向派生类的虚表)。
设计逻辑冲突
构造函数从基类到派生类依次执行。
若基类构造为虚函数,派生类可能会尝试调用自身版本的构造函数(此时派生类对象尚未完全构造),导致逻辑混乱。
语言规范限制
C++标准明确规定构造函数不能声明为virtual
,编译器会直接报错
13、 析构函数何时必须是虚函数?为什么?
必须声明为虚函数的场景:通过基类指针删除派生类对象时
class Base {
public:
virtual ~Base() { cout << "Base destructor" << endl; } // 虚析构函数
};
class Derived : public Base {
private:
int* data;
public:
Derived() { data = new int[10]; }
~Derived() override {
delete[] data; // 派生类资源清理
cout << "Derived destructor" << endl;
}
};
// 使用基类指针创建和删除派生类对象
Base* ptr = new Derived();
delete ptr; // 若Base析构非虚,则仅调用Base::~Base(),导致Derived::data未释放
声明基类析构函数为虚函数后,delete
基类指针时会先调用派生类析构函数,再调用基类析构函数,保证资源被完整释放。
14、默认构造函数与无参构造函数
默认构造函数是指无需参数即可调用的构造函数。包括:
- 编译器自动生成的构造函数:当类中未定义任何构造函数时,编译器隐式生成一个无参构造函数(称为 “合成默认构造函数”)。
- 用户显式定义的无参构造函数:如
Data() {}
或Data() = default
。
// 若类中定义了任何构造函数(无论是否带参数),编译器不会自动生成默认构造函数。因此,以下两种情况都会导致默认构造函数缺失
struct Data1 {
Data1(int x) {} // 带参构造函数 → 无默认构造函数
};
struct Data2 {
Data2() {} // 用户显式定义无参构造函数 → 编译器不再生成默认构造函数
};
默认构造函数是编译器自动生成或用户显式定义的无参数构造函数,或者所有参数均有默认值的构造函数(如
MyClass(int x = 0)
)。其核心特点是无需传递参数即可调用。class MyClass { public: MyClass(int x = 0, double y = 1.0) { /* 参数均有默认值 */ } }; // 此时,MyClass obj; 或 MyClass obj{}; 均可调用该构造函数
无参构造函数(Parameterless Constructor)
无参构造函数是参数列表为空的构造函数,属于默认构造函数的一种特例。
- 用户显式定义的、参数列表为空的构造函数。(如
MyClass() {}
) - 用户未定义任何构造函数时,编译器自动生成隐式无参构造函数
- 必须由用户手动编写,编译器不会自动生成(除非使用
= default
)
默认构造函数是一个更宽泛的概念,包括编译器生成的和用户定义的无参构造函数。
默认构造函数的显式声明方式
方式 1:用户自定义无参构造函数
class MyClass {
public:
MyClass() { // 自定义默认构造函数
// 初始化逻辑
}
};
方式 2:使用= default
强制编译器生成
class MyClass {
public:
MyClass() = default; // 显式要求编译器生成默认构造函数
};
当类定义了任何构造函数(包括无参构造函数)时,编译器不会自动生成默认构造函数。
可通过= default
显式请求。
class Data {
public:
Data(int val) : value(val) {} // 定义带参构造函数
Data() = default; // 显式生成默认构造函数
private:
int value;
};
派生类的构造函数会隐式调用基类的默认构造函数。若基类无默认构造函数,派生类必须显式调用基类的其他构造函数。
class Base {
public:
Base(int x) {} // 基类无默认构造函数
};
class Derived : public Base {
public:
Derived() : Base(0) {} // 必须显式调用Base的构造函数
};
聚合类(无用户声明构造函数、无私有 / 保护成员、无虚函数)可使用聚合初始化(花括号语法),即使没有默认构造函数。
struct Data {
int x;
Data(int x) : x(x) {} // 有带参构造函数,无默认构造函数
};
Data d{42}; // 合法:聚合初始化
Data d; // 非法:无默认构造函数
工程中的最佳实践:
- 若类需要默认初始化,即使为空,也显式声明
- 使用成员初始化器确保所有成员被正确初始化,避免基本类型未初始化
- 设计类接口时考虑默认构造的必要性
- 若类不允许无参初始化,显式删除默认构造函数
const
和引用成员:必须通过构造函数初始化列表初始化,隐式默认构造函数无法处理
class ConstDemo {
public:
const int c;
ConstDemo() : c(10) {} // 必须显式初始化
};
在C++中,每个类最多只能有一个默认构造函数,即以下两种形式不能同时存在:
- 无参构造函数(无参数)
- 全缺省参数构造函数(所有参数均有默认值)
若同时定义这两种形式,会导致编译错误
class Date {
public:
// 无参构造函数
Date() : _year(1), _month(1), _day(1) {} // 默认构造函数①
// 全缺省参数构造函数(等价于默认构造函数②)
Date(int year = 1, int month = 1, int day = 1)
: _year(year), _month(month), _day(day) {}
private:
int _year, _month, _day;
};
int main() {
Date d1; // 错误:存在多个默认构造函数
return 0;
}
15、讲讲构造函数的初始化列表
构造函数的初始化列表(Member Initializer List):高效,在对象创建时直接初始化成员,而非先默认初始化再赋值。
若不在初始化列表中显式初始化,成员会先默认初始化,再在构造函数体内赋值(两次操作)。
MyClass(int x) {
member = x; // 先默认初始化member,再赋值
}
直接初始化:初始化列表直接调用成员的构造函数,仅一次操作,效率更高。
必须使用初始化列表的场景
a、常量成员(const
)和引用成员(&
)
常量一旦创建就不能修改,初始化列表是在对象创建时直接初始化成员的唯一机会
class Example {
public:
Example(int& ref) : constVal(42), refVal(ref) {} // 必须在初始化列表中初始化
private:
const int constVal; // 常量成员
int& refVal; // 引用成员
};
b、没有默认构造函数的类成员
class NoDefault {
public:
NoDefault(int x) {} // 无默认构造函数
};
class Container {
public:
Container() : obj(42) {} // 必须在初始化列表中显式调用构造函数
private:
NoDefault obj;
};
c、基类没有默认构造函数
class Base {
public:
Base(int x) {} // 无默认构造函数
};
class Derived : public Base {
public:
Derived() : Base(42) {} // 必须在初始化列表中调用基类构造函数
};
注意事项
a、初始化顺序由成员声明顺序决定,初始化列表的书写顺序不影响实际初始化顺序
// 存在bug
class Example {
public:
Example(int x) : b(x), a(b) {} // 先初始化a(使用未初始化的b),再初始化b
private:
int a; // 先声明
int b; // 后声明
};
b、避免在初始化列表中使用成员函数 ,若成员函数依赖其他未初始化的成员,可能引发错误。
c、与默认参数的配合,当构造函数声明和实现分离时,默认参数必须写在声明中,而初始化列表只能写在实现处
class Example {
public:
Example(int a = 0, int b = 0); // 默认参数在声明中
};
Example::Example(int a, int b) : member1(a), member2(b) {} // 初始化列表在实现中
d、子类构造函数必须显式初始化基类成员(若基类无默认构造函数),且初始化顺序为基类先于派生类成员
class Base { /* ... */ };
class Derived : public Base {
int x;
public:
Derived(int val) : Base(val), x(val) {} // 先初始化基类,再成员x
};
总结
特性 | 初始化列表 | 构造函数体 |
---|---|---|
执行时机 | 在对象创建时直接初始化 | 在初始化后执行赋值操作 |
适用场景 | 常量成员、引用成员、无默认构造函数 | 复杂逻辑(如条件判断、循环) |
性能 | 通常更高效(一次初始化) | 可能低效(默认初始化 + 赋值) |
初始化顺序 | 由成员声明顺序决定 | 由代码顺序决定 |
C++11 后的扩展特性
允许在类定义时直接初始化成员,与初始化列表结合时,初始化列表优先。
class MyClass {
private:
int x = 0; // 默认初始化器
public:
MyClass(int val) : x(val) {} // 初始化列表覆盖默认值
};
一个构造函数调用另一个构造函数,减少代码重复。
class MyClass {
public:
MyClass() : MyClass(0) {} // 委托给另一个构造函数
MyClass(int x) : data(x) {}
private:
int data;
};
16、 类中static
、const
、virtual
修饰的特殊成员函数,explicit、final、override
a、static
成员函数
- 属于类而非对象实例,无需创建对象即可调用(通过
类名::函数名
)。 - 无
this
指针:无法访问非静态成员(变量 / 函数),只能操作静态成员 - 不能声明为
virtual
、const(const
需要隐式this
指针保护对象状态)
或volatile
。
class MathUtils {
public:
static double PI; // 静态成员变量
static double sqrt(double x); // 静态成员函数
};
double MathUtils::PI = 3.14159; // 类外初始化
添加 static
会导致编译器尝试重新定义静态变量,引发重复定义错误。
// 类外初始化(错误写法)
static double MathUtils::PI = 3.14159; // ❌ 编译错误:重复 static
非 const
的静态成员变量不能在类内直接初始化。
- 静态成员变量属于类本身而非对象实例,其存储空间需要在类外分配。
- 类内声明仅定义变量的属性(类型、名称、
static
修饰),不分配内存。 - 类外初始化的作用是定义存储空间并赋初值。
class MathUtils {
public:
static double PI = 3.14; // ❌ 编译错误:非 const 静态成员不能在类内初始化
};
const
静态成员变量
- 整型/枚举类型:可在类内直接初始化
- 非整型(如
double
):仍需类外定义。
class MathUtils {
public:
static const int MAX_VALUE = 100; // 合法(整型)
static const double PI; // 非整型需类外初始化
};
// 类外定义
const double MathUtils::PI = 3.14;
C++17 内联静态成员(inline static
)
- 允许在类内直接初始化非
const
静态成员,无需类外定义。 - 适用于所有类型(包括
double
)
class MathUtils {
public:
inline static double PI = 3.14; // ✅ C++17 起合法
};
b、const
成员函数
- 可被
const
对象调用,非const
函数无法被const
对象调用 const
对象的this
指针为const T*
,只能匹配this
类型为const T*
的函数(即const
成员函数),以确保对象状态不被修改。
class Point {
public:
int x() const { return _x; } // const函数
void setX(int x) { _x = x; } // 非const函数
private:
int _x;
};
const Point p(1, 2);
p.x(); // 合法
p.setX(3); // 非法:const对象不能调用非const函数
重载区分:根据对象是否为 const
选择不同实现
class Container {
public:
int& operator[](size_t idx); // 非const版本(返回引用可修改)
const int& operator[](size_t idx) const; // const版本(返回const引用)
};
mutable
成员例外:mutable
修饰的变量可在 const
函数中修改(如缓存、计数器)
c、virtual
成员函数
通过基类指针 / 引用调用时,根据实际对象类型决定调用的函数版本。
使用 = 0
声明纯虚函数,包含纯虚函数的类为抽象类,不可实例化
class Shape {
public:
virtual double area() const = 0; // 纯虚函数
};
class Circle : public Shape {
public:
double area() const override { return 3.14 * _r * _r; }
private:
double _r;
};
d、explicit
构造函数
禁止隐式转换:阻止单参数构造函数被用于隐式类型转换。
class String {
public:
explicit String(int size); // 显式构造函数
};
String s = 10; // 非法:禁止隐式转换
String s(10); // 合法:显式调用构造
e、final
与 override
override
显式重写:确保函数正确重写基类虚函数,编译器检查签名匹配。
class Base {
public:
virtual void func(int) {}
};
class Derived : public Base {
public:
void func(int) override {} // 必须与Base::func签名一致
};
final
禁止重写 / 继承
class Base {
public:
virtual void func() final {} // 此虚函数不可被派生类重写
};
class FinalClass final {}; // 此类不可被继承
17、 类成员指针的定义与使用错误
#include <iostream>
using namespace std;
class Test {
public:
void func() { cout << "call Test::func" << endl; }
static void static_func() { cout << "Test::static_func" << endl; }
int ma;
};
int main() {
// 非静态成员变量指针(类成员指针)
int Test::*p_ma = &Test::ma; // 正确定义,而不是int *p_ma = &Test::ma
Test obj;
obj.*p_ma = 20; // 通过对象访问成员指针,设置ma的值
// 静态成员函数指针(普通指针,因为静态成员属于类)
void (*p_static_func)() = &Test::static_func;
p_static_func(); // 调用静态函数
// 非静态成员函数指针必须通过对象实例调用
void (Test::*pfunc)() = &Test::func;
(obj.*pfunc)(); // 格式:(对象.*指针)()
// (*pfunc)() 对非静态成员函数指针是错误的,因为它缺少对象绑定。
// (*pfunc)() 试图直接调用成员函数指针,没有提供对象实例。这会导致编译器无法确定 this 指针的值,因此报错。
return 0;
}
Test::ma
是非静态成员变量,其指针类型为 int Test::*
(指向类成员的指针),而非普通的 int*
。普通指针 int*
用于指向对象实例的内存,而类成员指针需绑定到具体对象才能访问。