嵌入式八股文学习——基类构造与析构、成员初始化及继承特性详解
文章目录
- 基类的构造函数与析构函数是否能被派生类继承?
- 需要使用初始化列表而不能使用赋值**
- 1. 常量成员(`const`)
- 例子:
- 2. 引用成员(`reference`)
- 例子:
- 3. 没有默认构造函数的成员类型
- 例子:
- 4. **派生类需要调用基类的构造函数**
- 例子:
- 总结
- 类的成员变量的初始化顺序是什么
- 1. 成员变量的初始化顺序与定义顺序有关
- 示例:
- 2. **构造函数内初始化**
- 示例:
- 3. 类成员变量在定义时不能初始化
- 示例:
- 4. `const` 成员变量的初始化
- 示例:
- 5. `static` 成员变量的初始化
- 示例:
- 6. **静态变量的初始化顺序**
- 7. **构造函数的初始化顺序**
- 示例:
- 总结
- 当一个类为另一个类的成员变量时,如何对其进行初始化?
- 1. **通过初始化列表初始化成员对象**
- 示例:
- 2. 通过默认构造函数初始化
- 示例:
- 3. **如果成员类没有默认构造函数**
- 示例:
- 4. **使用成员类的默认值或常量初始化**
- 示例:
- 总结:
- 标记类不可继承
- 方法一:私有化构造函数和析构函数
- 示例代码:
- 解析:
- 方法二:使用模板类和虚拟继承
- 示例代码:
- 解析:
- 总结:
- 构造函数没有返回值,那么如何得知对象是否构造成功?
- 如何处理构造失败:
- 示例代码:
- 代码解析:
- 构造函数抛出异常的影响:
- 总结:
- Public继承、protected继承、private继承的区别?
- 公有继承、保护继承、私有继承的区别
- 具体说明:
- 示例代码:
- 总结:
基类的构造函数与析构函数是否能被派生类继承?
在C++中,基类的构造函数和析构函数不能被派生类继承。具体来说:
-
基类的构造函数不能被派生类继承。派生类必须显式声明自己的构造函数,并在构造函数的初始化列表中调用基类的构造函数来初始化基类的成员。派生类可以通过调用基类的构造函数来完成基类成员的初始化,但基类的构造函数不会被自动继承。
例如:
class Base { public: Base(int x) { // 基类构造函数 } }; class Derived : public Base { public: Derived(int x) : Base(x) { // 显式调用基类构造函数 // 派生类构造函数 } };
-
基类的析构函数也不能被派生类继承。派生类需要声明自己的析构函数。尽管析构函数不能被继承,但派生类的析构函数会自动调用基类的析构函数,因此我们不需要显式调用基类的析构函数。
例如:
class Base { public: ~Base() { // 基类析构函数 } }; class Derived : public Base { public: ~Derived() { // 派生类析构函数 // 派生类析构函数的代码 } };
析构函数的调用顺序是从派生类到基类,即先调用派生类的析构函数,然后自动调用基类的析构函数。
需要使用初始化列表而不能使用赋值**
1. 常量成员(const
)
常量成员只能在对象构造时初始化,之后无法赋值。因此,必须使用初始化列表来初始化 const
成员。
例子:
class Example {
const int cval; // 常量成员
public:
Example() : cval(10) {} // 只能在初始化列表中初始化
};
如果尝试在构造函数体内赋值,会导致编译错误:
Example::Example() {
cval = 10; // 错误:不能对const成员赋值
}
2. 引用成员(reference
)
引用成员必须在对象构造时进行初始化,且只能被初始化一次。引用成员不能被赋值,因此也必须在初始化列表中初始化。
例子:
class Example {
int& rval; // 引用成员
public:
Example(int& val) : rval(val) {} // 必须通过初始化列表初始化引用成员
};
如果尝试在构造函数体内赋值,也会导致编译错误:
Example::Example(int& val) {
rval = val; // 错误:引用成员不能赋值
}
3. 没有默认构造函数的成员类型
如果类的成员类型没有默认构造函数(即没有无参构造函数),则必须通过初始化列表来显式地初始化这些成员类型。否则,编译器无法自动为这些成员类型调用默认构造函数。
例子:
class MyClass {
public:
MyClass(int x) { } // 没有默认构造函数
};
class Example {
MyClass obj; // 成员类型没有默认构造函数
public:
Example() : obj(10) {} // 必须通过初始化列表初始化
};
在这个例子中,MyClass
没有默认构造函数,因此 obj
必须通过初始化列表来显式初始化。
4. 派生类需要调用基类的构造函数
在继承中,如果基类没有默认构造函数(即没有无参构造函数),那么派生类必须通过初始化列表显式调用基类的构造函数来初始化基类成员。
例子:
class Base {
public:
Base(int x) { }
};
class Derived : public Base {
public:
Derived() : Base(10) {} // 必须在初始化列表中调用基类构造函数
};
如果基类没有默认构造函数,派生类构造函数就必须显式调用基类的构造函数。
总结
在 C++ 中,初始化列表和赋值的主要区别在于,初始化列表在对象构造时进行初始化,而赋值是在对象已构造后进行修改。以下情况必须使用初始化列表:
const
类型成员- 引用成员(
reference
) - 没有默认构造函数的成员类型
- 派生类构造函数中调用基类构造函数
类的成员变量的初始化顺序是什么
在 C++ 中,类成员变量的初始化顺序是由一些规则决定的,具体包括以下几点:
1. 成员变量的初始化顺序与定义顺序有关
成员变量的初始化顺序是由它们在类中的声明顺序决定的,而不是初始化列表中列出的顺序。即使在构造函数中,成员变量的初始化顺序与它们在初始化列表中的位置无关,而是按照它们在类定义中出现的顺序进行初始化。
示例:
class Example {
int a;
double b;
public:
Example() : b(3.14), a(10) {} // 但成员变量的初始化顺序是先 a,再 b
};
在上面的例子中,尽管初始化列表中 b
在 a
之前,但 a
仍然会先被初始化,因为它在类定义中排在 b
之前。
2. 构造函数内初始化
如果不使用初始化列表,而是在构造函数体内初始化成员变量,那么初始化的顺序将取决于成员变量在构造函数中的顺序。
示例:
class Example {
int a;
double b;
public:
Example() {
a = 10; // 先初始化 a
b = 3.14; // 后初始化 b
}
};
在这种情况下,初始化顺序是按构造函数中代码的书写顺序来确定的,而不是类定义中的顺序。
3. 类成员变量在定义时不能初始化
在类中,成员变量不能在定义时直接初始化,除非它们是 static
或 const
类型,或者是类内的构造函数初始化列表中。
示例:
class Example {
int a = 10; // 错误:成员变量不能在类定义时直接初始化,除非是 C++11 及以后版本
};
但从 C++11 起,可以在类定义中对 const
或 constexpr
类型的成员进行初始化:
class Example {
const int a = 10; // 正确:const 成员可以在类定义时初始化
};
4. const
成员变量的初始化
const
成员变量必须通过初始化列表进行初始化,不能在构造函数体内赋值。
示例:
class Example {
const int a;
public:
Example() : a(10) {} // 必须在初始化列表中初始化
};
5. static
成员变量的初始化
static
成员变量不能在类内部初始化,必须在类外部进行初始化。通常,static
成员变量需要为每个类实例共享,因此它们的初始化是在类外部完成的。
示例:
class Example {
static int a;
public:
static void setA(int value) { a = value; }
};
int Example::a = 10; // 类外初始化 static 成员
6. 静态变量的初始化顺序
全局变量和静态变量的初始化顺序是由链接顺序决定的。首先初始化的是基类的静态成员,然后才是派生类的静态成员。静态成员的初始化顺序和类的继承层次有关,但无法通过普通的代码控制。
例如:
- 先初始化基类的静态成员
- 然后初始化派生类的静态成员
需要注意的是,静态变量和全局变量的初始化顺序是不确定的,因此程序中如果依赖于静态变量初始化的顺序,可能会导致未定义的行为。
7. 构造函数的初始化顺序
在构造函数中,首先初始化基类的成员变量,然后初始化派生类的成员变量。构造函数的执行次序是从基类到派生类。
示例:
class Base {
protected:
int baseValue;
public:
Base(int value) : baseValue(value) {}
};
class Derived : public Base {
int derivedValue;
public:
Derived(int base, int derived) : Base(base), derivedValue(derived) {}
};
在上述例子中,Base
类的成员 baseValue
会先被初始化,然后才是 Derived
类的成员 derivedValue
。
总结
类成员变量的初始化顺序由以下规则决定:
- 成员变量初始化顺序与它们在类定义中的顺序相关,而不是在初始化列表中的顺序。
- 使用初始化列表时,必须遵循成员变量在类中的声明顺序。
const
成员变量必须在初始化列表中初始化,不能在构造函数体内赋值。static
成员变量需要在类外初始化。- 静态变量的初始化顺序取决于它们的声明顺序及继承关系,且全局和静态变量的初始化顺序可能存在不确定性。
当一个类为另一个类的成员变量时,如何对其进行初始化?
当一个类作为另一个类的成员变量时,初始化方式依赖于成员变量所属类的构造函数以及是否使用初始化列表来进行初始化。可以通过以下几种方法来初始化成员变量:
1. 通过初始化列表初始化成员对象
在构造函数的初始化列表中,使用成员类的构造函数来初始化成员变量。
示例:
假设我们有两个类 ClassA
和 ClassB
,其中 ClassB
是 ClassA
的成员变量。
class ClassB {
public:
ClassB(int x) {
// 构造函数初始化 ClassB 的成员
std::cout << "ClassB constructed with " << x << std::endl;
}
};
class ClassA {
ClassB obj; // ClassB 是 ClassA 的成员变量
public:
// 在 ClassA 的构造函数中初始化 obj
ClassA(int val) : obj(val) {
// obj 会被 ClassB 的构造函数初始化
std::cout << "ClassA constructed" << std::endl;
}
};
int main() {
ClassA a(10); // ClassA 的构造函数会初始化 obj
return 0;
}
在这个例子中:
ClassA
的构造函数通过初始化列表: obj(val)
来调用ClassB
的构造函数,并传递参数val
来初始化ClassB
的成员对象obj
。- 这就是通过初始化列表来初始化成员对象的方式。
2. 通过默认构造函数初始化
如果成员对象的类提供了默认构造函数(即没有参数的构造函数),则可以直接通过初始化列表或在类定义时进行默认初始化。
示例:
class ClassB {
public:
ClassB() {
// 默认构造函数
std::cout << "ClassB default constructed" << std::endl;
}
};
class ClassA {
ClassB obj; // ClassB 是 ClassA 的成员变量
public:
// ClassA 的构造函数
ClassA() {
// ClassB 的默认构造函数会被调用
std::cout << "ClassA constructed" << std::endl;
}
};
int main() {
ClassA a; // ClassA 的构造函数会初始化 obj
return 0;
}
在这个例子中:
ClassB
提供了一个默认构造函数。ClassA
的构造函数不需要显式地初始化obj
,因为ClassB
的默认构造函数会自动被调用来初始化obj
。
3. 如果成员类没有默认构造函数
如果成员类没有默认构造函数,则必须在初始化列表中显式调用该类的构造函数,并传递适当的参数。
示例:
class ClassB {
public:
ClassB(int x) {
// 构造函数需要一个参数
std::cout << "ClassB constructed with " << x << std::endl;
}
};
class ClassA {
ClassB obj; // ClassB 是 ClassA 的成员变量
public:
// 必须通过初始化列表初始化 obj
ClassA(int val) : obj(val) { // obj 被 ClassB 的构造函数初始化
std::cout << "ClassA constructed" << std::endl;
}
};
int main() {
ClassA a(20); // ClassA 的构造函数会初始化 obj
return 0;
}
在这个例子中:
ClassB
没有默认构造函数,只能通过一个带有参数的构造函数来初始化。- 因此,
ClassA
必须在初始化列表中显式地调用ClassB
的构造函数,并传递参数val
。
4. 使用成员类的默认值或常量初始化
如果成员类的成员变量有默认值或常量初始值,且不需要通过外部传入参数来初始化,也可以通过默认构造函数或使用类定义时的初始值来初始化。
示例:
class ClassB {
public:
int val;
ClassB(int x = 10) : val(x) {} // 默认值
};
class ClassA {
ClassB obj; // ClassB 是 ClassA 的成员变量
public:
// ClassA 的构造函数会使用 ClassB 的默认构造函数
ClassA() : obj(5) { // 使用初始化列表初始化 obj,给 val 赋值 5
std::cout << "ClassA constructed" << std::endl;
}
};
int main() {
ClassA a; // obj 的 val 会初始化为 5
return 0;
}
在这个例子中:
ClassB
提供了一个带有默认值的构造函数,因此即使在ClassA
的构造函数中没有传递参数,ClassB
也可以用默认值初始化。
总结:
- 初始化列表 是用来初始化类的成员变量的标准方法,尤其是当成员对象有非默认构造函数时。
- 默认构造函数 可在没有传递参数时自动初始化成员对象。
- 显式初始化 在某些情况下(如没有默认构造函数或需要特定参数初始化)必须使用初始化列表来初始化成员对象。
标记类不可继承
方法一:私有化构造函数和析构函数
我们可以将类的构造函数和析构函数声明为私有的,并提供一个静态方法来创建和销毁类的实例。这样一来,其他类就无法继承它,因为它们无法访问构造函数和析构函数。
示例代码:
class FinalClass1 {
public:
// 提供静态方法来获取实例
static FinalClass1* GetInstance() {
return new FinalClass1;
}
// 提供静态方法来删除实例
static void DeleteInstance(FinalClass1* pInstance) {
delete pInstance;
pInstance = nullptr;
}
private:
// 将构造函数和析构函数私有化,防止继承
FinalClass1() {
std::cout << "FinalClass1 constructed" << std::endl;
}
~FinalClass1() {
std::cout << "FinalClass1 destructed" << std::endl;
}
};
// 使用示例
int main() {
FinalClass1* p = FinalClass1::GetInstance(); // 通过静态方法创建实例
FinalClass1::DeleteInstance(p); // 通过静态方法销毁实例
return 0;
}
解析:
FinalClass1
的构造函数和析构函数都被私有化,因此无法从其他类继承它。GetInstance()
静态方法返回一个动态分配的对象,使得该类只能在堆上创建实例。DeleteInstance()
静态方法提供了删除实例的功能。
方法二:使用模板类和虚拟继承
通过定义一个模板类 MakeFinal
,使得想要被禁止继承的类继承这个模板类,并使用虚继承的特性,保证即使子类可以访问构造函数,也无法继承类。
示例代码:
template <typename T>
class MakeFinal {
friend T; // 允许 T 类访问 MakeFinal 的私有构造和析构函数
private:
MakeFinal() {
std::cout << "MakeFinal constructed" << std::endl;
}
~MakeFinal() {
std::cout << "MakeFinal destructed" << std::endl;
}
};
class FinalClass2 : virtual public MakeFinal<FinalClass2> {
public:
FinalClass2() {
std::cout << "FinalClass2 constructed" << std::endl;
}
~FinalClass2() {
std::cout << "FinalClass2 destructed" << std::endl;
}
};
// 试图继承 FinalClass2 会失败
class Try : public FinalClass2 {
public:
Try() {}
~Try() {}
}; // 编译错误:无法访问 MakeFinal 的私有构造函数
int main() {
FinalClass2 f2; // 可以实例化 FinalClass2
// Try temp; // 编译错误:Try 无法继承 FinalClass2
return 0;
}
解析:
MakeFinal
是一个模板类,其中的构造函数和析构函数都是私有的,只有类的友元T
(在这里是FinalClass2
)可以访问这些私有成员。FinalClass2
通过虚拟继承MakeFinal<FinalClass2>
来防止被继承。虽然FinalClass2
是MakeFinal<FinalClass2>
的友元类,可以访问私有成员,但是一旦尝试从FinalClass2
继承一个新类(比如Try
),就会失败,因为Try
无法访问MakeFinal<FinalClass2>
的私有构造函数。
总结:
- 方法一 通过将构造函数和析构函数私有化,阻止了继承,但只能通过静态方法在堆上创建实例,且无法在栈上创建。
- 方法二 使用模板类和虚继承,利用 C++ 中的友元机制和虚继承机制,可以防止继承并提供较为灵活的使用方式,能够在栈上和堆上创建实例。
这两种方法都能有效地阻止类被继承,具体选择哪种方式取决于具体的需求。
构造函数没有返回值,那么如何得知对象是否构造成功?
在 C++ 中,构造函数没有返回值,因此我们不能直接通过返回值来判断对象是否成功构造。为了处理对象构造失败的情况,通常的做法是在构造函数中抛出异常。这样,当构造过程失败时,可以通过异常机制通知调用者。
如何处理构造失败:
-
抛出异常:在构造函数中,我们可以执行必要的初始化操作(例如打开文件、连接数据库等),如果这些操作失败,可以抛出异常,表示构造失败。
-
析构函数的执行:当构造函数抛出异常时,对象的生命周期被终止,并且已经构造完成的成员对象将按照相反的顺序被析构。这是 C++ 的异常处理机制保证的。
-
部分构造:如果在构造过程中某些成员变量成功初始化,但某些操作失败并抛出异常,那么之前已成功构造的部分会被自动清理,避免资源泄漏。
示例代码:
以下是一个简单的例子,展示了如何在构造函数中抛出异常来处理初始化失败的情况。
#include <iostream>
#include <stdexcept> // 用于抛出异常
class DatabaseConnection {
public:
DatabaseConnection(const std::string& dbName) {
std::cout << "Trying to connect to database: " << dbName << std::endl;
// 假设打开数据库时失败
if (dbName.empty()) {
throw std::runtime_error("Failed to connect to the database: Invalid database name.");
}
// 模拟数据库连接成功
std::cout << "Successfully connected to the database." << std::endl;
}
~DatabaseConnection() {
std::cout << "Database connection closed." << std::endl;
}
};
class FileManager {
public:
FileManager(const std::string& fileName) {
std::cout << "Trying to open file: " << fileName << std::endl;
// 假设打开文件时失败
if (fileName.empty()) {
throw std::runtime_error("Failed to open file: Invalid file name.");
}
// 模拟文件打开成功
std::cout << "File opened successfully." << std::endl;
}
~FileManager() {
std::cout << "File closed." << std::endl;
}
};
class MyClass {
private:
DatabaseConnection db;
FileManager file;
public:
MyClass(const std::string& dbName, const std::string& fileName)
: db(dbName), file(fileName) { // 通过构造函数初始化成员变量
std::cout << "MyClass constructed." << std::endl;
}
~MyClass() {
std::cout << "MyClass destructed." << std::endl;
}
};
int main() {
try {
// 构造对象时,如果其中一个成员的构造失败,将抛出异常
MyClass obj("", "myfile.txt"); // 数据库连接失败,因为传入了空字符串
}
catch (const std::exception& e) {
std::cerr << "Exception caught: " << e.what() << std::endl;
}
return 0;
}
代码解析:
- DatabaseConnection 和 FileManager 是两个需要在构造时执行一些初始化操作的类,例如连接数据库和打开文件。如果数据库名称或文件名为空,它们将抛出
std::runtime_error
异常来表示初始化失败。 - 在
MyClass
的构造函数中,先构造DatabaseConnection
对象,再构造FileManager
对象。如果DatabaseConnection
的构造失败(抛出异常),则FileManager
的构造不会被调用,且已构造的部分(即DatabaseConnection
)会被自动销毁。 - 异常被捕获并输出错误信息,程序会优雅地结束。
构造函数抛出异常的影响:
- 当构造函数抛出异常时,已经完成初始化的成员对象(包括继承自基类的成员)会按相反顺序被销毁,确保资源得以释放。
- 构造失败的对象不会被创建,这意味着程序可以在异常处理机制中做出适当反应,如日志记录、回滚操作等。
总结:
由于构造函数没有返回值,我们通过在构造函数中抛出异常来标识构造是否成功。这种做法可以处理一些复杂的初始化问题,如打开文件、连接数据库等,并能够在构造失败时适当清理已分配的资源。
Public继承、protected继承、private继承的区别?
在 C++ 中,继承有三种主要的访问权限:公有继承(public inheritance)、保护继承(protected inheritance) 和 私有继承(private inheritance)。它们在派生类中对基类成员的访问控制方式不同。下面通过表格来总结这三种继承方式的主要区别。
公有继承、保护继承、私有继承的区别
特性 | 公有继承 (public inheritance) | 保护继承 (protected inheritance) | 私有继承 (private inheritance) |
---|---|---|---|
基类的公有成员 | 仍然是公有的(public ) | 变为保护的(protected ) | 变为私有的(private ) |
基类的保护成员 | 仍然是保护的(protected ) | 仍然是保护的(protected ) | 变为私有的(private ) |
基类的私有成员 | 不可访问(private ) | 不可访问(private ) | 不可访问(private ) |
派生类的访问权限 | 公有成员和保护成员可以被派生类访问 | 公有成员和保护成员可以被派生类访问,但它们在派生类中是保护的 | 基类成员都变为私有的,派生类只能通过成员函数访问 |
基类成员对外的访问 | 基类的公有成员可以被外部访问 | 基类的公有成员和保护成员对外不可访问 | 基类的公有成员和保护成员对外不可访问 |
使用场景 | 适用于“是一个”的关系,表示派生类可以继承基类的行为和特性 | 适用于派生类需要继承基类,但不希望基类的成员对外可见 | 适用于“实现”的关系,派生类并不打算暴露基类的行为和特性 |
具体说明:
-
公有继承(public inheritance):
- 这是最常用的继承方式,符合面向对象设计中的**“是一个(is-a)”**关系。例如,一个
Dog
是一个Animal
,继承自基类Animal
的派生类Dog
可以访问基类的公有和保护成员。 - 在这种继承方式下,基类的公有成员会在派生类中保持公有,保护成员会保持保护状态,而基类的私有成员则无法访问。
- 这是最常用的继承方式,符合面向对象设计中的**“是一个(is-a)”**关系。例如,一个
-
保护继承(protected inheritance):
- 保护继承适用于那些希望派生类能够继承基类的行为,但不希望基类的成员暴露给外部访问的情况。继承者继承了基类的成员,但这些成员在派生类中是保护的。
- 在这种方式下,基类的公有成员和保护成员都会变成派生类的保护成员,而基类的私有成员依然无法访问。
-
私有继承(private inheritance):
- 私有继承是“实现(implementation)”的关系,意味着派生类并不打算将基类的公有行为暴露给外部使用。在这种情况下,基类的所有成员(公有和保护成员)都会变成派生类的私有成员。
- 私有继承通常用于派生类仅依赖基类的实现细节,而不需要让外部用户访问基类的接口。
示例代码:
#include <iostream>
using namespace std;
// 基类
class Base {
public:
void publicMethod() { cout << "Base publicMethod" << endl; }
protected:
void protectedMethod() { cout << "Base protectedMethod" << endl; }
private:
void privateMethod() { cout << "Base privateMethod" << endl; }
};
// 公有继承
class PublicDerived : public Base {
public:
void test() {
publicMethod(); // 可以访问基类的公有方法
protectedMethod(); // 可以访问基类的保护方法
// privateMethod(); // 错误:无法访问基类的私有方法
}
};
// 保护继承
class ProtectedDerived : protected Base {
public:
void test() {
publicMethod(); // 可以访问基类的公有方法
protectedMethod(); // 可以访问基类的保护方法
// privateMethod(); // 错误:无法访问基类的私有方法
}
};
// 私有继承
class PrivateDerived : private Base {
public:
void test() {
publicMethod(); // 可以访问基类的公有方法
protectedMethod(); // 可以访问基类的保护方法
// privateMethod(); // 错误:无法访问基类的私有方法
}
};
int main() {
PublicDerived pd;
pd.test(); // 公有继承:可以访问公有和保护方法,但无法访问私有方法
ProtectedDerived ptd;
ptd.test(); // 保护继承:可以访问公有和保护方法,但无法访问私有方法
PrivateDerived prd;
prd.test(); // 私有继承:可以访问公有和保护方法,但无法访问私有方法
return 0;
}
总结:
- 公有继承适用于“是一个”关系,派生类的行为与基类保持一致,允许对基类公有成员的访问。
- 保护继承适用于“部分是一个”关系,派生类可以使用基类的成员,但这些成员不能被外部直接访问。
- 私有继承适用于“实现”关系,派生类完全封装了基类的行为和特性,基类的公有和保护成员在派生类中变为私有,外部无法访问。