突破编程_C++_基础教程(类的高级特性)
1 嵌套类
嵌套类是指在一个类的内部定义另一个类。嵌套类和成员变量以及成员函数很相似,也可以是公有、保护或私有的。嵌套类在使用上有点像是一个命名空间,可以将相关的类组织在一起,提高代码的可读性和可维护性。典型的比如使用嵌套类实现工厂模式:
#include <iostream>
#include <string>
using namespace std;
// 外部类,作为工厂类的容器
class AnimalFactory
{
public:
// 嵌套类,定义不同的动物
class Animal
{
public:
virtual void eat() {}
};
// 嵌套类,实现具体的动物类(猫)
class Cat : public Animal
{
public:
void eat()
{
printf("cat eat\n");
}
};
// 嵌套类,实现具体的动物类(狗)
class Dog : public Animal
{
public:
void eat()
{
printf("dog eat\n");
}
};
// 工厂方法,根据传入的字符串创建相应的动物
static std::shared_ptr<Animal> createAnimal(const std::string& animalType) {
if ("Cat" == animalType)
{
return std::shared_ptr<Animal>(new Cat);
}
else if ("Dog" == animalType)
{
return std::shared_ptr<Animal>(new Dog);
}
return nullptr; // 如果类型不匹配,返回空指针
}
};
int main()
{
std::shared_ptr<AnimalFactory::Animal> cat = AnimalFactory::createAnimal("Cat");
cat->eat();
std::shared_ptr<AnimalFactory::Animal> dog = AnimalFactory::createAnimal("Dog");
dog->eat();
return 0;
}
上面代码的输出为:
cat eat
dog eat
在这个例子中,AnimalFactory 类包含了 3 嵌套类 Animal 、Cat 和 Dog,Cat 类和 Dog 类分别实现了 Animal 基类的 eat 方法。AnimalFactory 类还提供了一个静态工厂方法 createAnimal,该方法根据传入的字符串参数创建并返回相应类型的动物对象。main 函数中展示了如何使用工厂方法来创建和使用不同的对象。
使用嵌套类实现工厂模式的好处是,所有与形状相关的类都集中在 AnimalFactory 类中,这有助于保持代码的整洁和组织性。此外,通过将工厂方法放在 AnimalFactory 类中,可以确保只有这个类能够创建形状对象,这有助于控制对象的创建过程。
1.1 嵌套类的访问权限
嵌套类的访问权限遵循一般的类成员访问权限规则。嵌套类可以是 public 、 protected 或 private ,这取决于它们在外部类中的声明方式。这些访问修饰符决定了嵌套类在外部类的外部可见性和可访问性。
(1) public 嵌套类
如果嵌套类被声明为 public ,则它在外部类的外部是可见的,并且可以被任何能够访问外部类的代码所访问和使用。
(2) protected 嵌套类
如果嵌套类被声明为 protected ,则它在外部类的外部是不可见的,但可以被外部类的派生类访问。注意:一般很少将嵌套类定义成 protected 。
(3)Private嵌套类
如果嵌套类被声明为 private ,则它在外部类的外部是不可见的,并且只能被外部类的成员函数访问。
1.2 嵌套类的作用
(1)封装与代码组织
嵌套类允许将紧密相关的类组织在一起,提高代码的可读性和可维护性。
通过将嵌套类或实现细节隐藏在外部类内部,只通过外部类提供的接口暴露必要的功能,可以减少命名冲突和不必要的全局命名空间污染,也更符合面向对象编程的封装原则。
(2)提供类型安全性
通过在外部类中定义嵌套类,可以确保只有与该外部类相关的代码才能使用这种类型。
这有助于提高代码的类型安全性和减少逻辑错误。
(3)实现特定的设计模式
嵌套类在实现某些设计模式(如工厂模式、组合模式等)时非常有用。
通过嵌套类,可以更容易地表达类之间的关系和职责。
(4)减少编译依赖
当修改嵌套类时,只有包含该嵌套类的外部类需要重新编译,这有助于减少编译时间和构建系统的复杂性。
2 类枚举
注:该部分内容涉及到 C++11 新特性。
类枚举( class enumeration )是C++11引入的一项特性,也称为作用域枚举( scoped enumeration ),是在类内部定义的枚举(enum)类型。它提供了更强的类型安全性和作用域控制。
2.1 类枚举的定义与使用
类枚举在定义时会在其前面加上 enum class 或 enum struct 关键字,而不是传统的 enum 。这样做的好处是枚举值不会隐式地转换为整数,也不会与其他类型的枚举共享相同的命名空间,从而减少了潜在的错误和冲突。
如下为样例代码:
enum class Color {
RED,
GREEN,
BLUE
};
Color color1 = Color::RED; // OK
int color2 = Color::RED; // 错误:枚举值不会隐式地转换为整数
Color color1 = 0; // 错误:整数不能直接赋值给类枚举变量
上面代码中,定义了一个名为 Color 的 enum class ,它有三个枚举值: RED 、 GREEN 和 BLUE 。由于它是 enum class ,因此它的枚举值具有作用域,这意味着不能直接使用 RED ,而需要使用 Color::RED 来引用它。
需要注意的是,由于 enum class 是强类型的,因此不能直接将一个 enum class 值转换为其底层整数类型,也不能将整数隐式地转换为 enum class 值。如果需要执行这样的转换,需要使用静态转换函数。
2.2 定义可以接受任意枚举值的函数
此外,由于 enum class的 作用域限制,当需要定义一个可以接受任意枚举值的函数时,你不能直接使用枚举类型作为参数类型。相反,你需要使用模板或者将枚举值封装在一个可以接受任何类型的容器中(如 std::variant 或 std::any )。如下为样例代码:
#include <iostream>
#include <string>
using namespace std;
enum class Color {
RED,
GREEN,
BLUE
};
enum class Size {
SMALL,
MEDIUM,
LARGE
};
template <typename Enum>
void printEnumValue(Enum enumVal)
{
int val = static_cast<int>(enumVal);
printf("enum value = %d\n",val);
}
int main()
{
printEnumValue(Color::RED);
printEnumValue(Size::MEDIUM);
return 0;
}
上面代码的输出为:
enum value = 0
enum value = 1
在这个例子中, printEnumValue 是一个模板函数,它接受一个类型为 Enum 的参数,Enum 是一个模板参数,可以是任何类型。在函数内部,首先将枚举值转换为整数,然后将其打印出来。
3 静态成员
类的静态成员是属于类本身而不是类的任何特定对象的成员。静态成员可以是成员变量或成员函数。静态成员在类的所有对象之间是共享的,即它们只有一个副本,无论创建了多少个类的对象。
静态成员变量在类定义之外进行初始化,而且只需要初始化一次。初始化通常在类定义的外部在全局范围内完成,或者在类定义的内部使用 inline 初始化( C++11 及以后的版本允许)。静态成员函数只能访问静态成员变量或其他静态成员函数,它们不能访问非静态成员。
如下为样例代码:
#include <iostream>
class MyClass
{
public:
MyClass() {}
~MyClass() {}
public:
static void printStaticVal()
{
printf("static value = %d\n", m_staticVal);
}
static void setStaticVal(int val)
{
m_staticVal = val;
}
private:
static int m_staticVal;
};
int MyClass::m_staticVal = 0;
int main()
{
MyClass::setStaticVal(1);
MyClass::printStaticVal();
return 0;
}
上面代码的输出为:
static value = 1
上面代码中, MyClass 有一个静态成员变量 m_staticVal 和两个静态成员函数 printStaticVal 和 setStaticVal 。这些静态成员可以被类的任何对象访问,也可以直接通过类名访问。当通过 MyClass::setStaticVal 设置 m_staticVal 的值时,所有 m_staticVal 的对象都会看到相同的值,因为静态成员是共享的。
静态成员常用于实现与类相关的计数器、状态变量或用于访问类级别的资源,这些资源不需要与类的特定实例关联。
4 内联函数
内联函数是一种优化技术,用于减少函数调用的开销。当一个函数被声明为内联时,编译器会尝试在每个调用该函数的地方直接插入(或替换)该函数的代码,而不是进行常规的函数调用。这可以消除参数传递、栈帧创建和函数返回的开销,但可能会增加生成的代码的大小。
类内联函数是指定义为类成员的内联函数。它们通常用于小型、快速执行的函数,这些函数在类中被频繁调用,且代码量不大,适合直接内联展开。
如下为样例代码:
class MyClass
{
public:
MyClass() {}
~MyClass() {}
public:
void func1() // 内联函数:如果成员函数的定义在类定义内部,则会被隐式地视为内联的
{
printf("func1\n");
}
inline void func2(); //内联函数
};
void MyClass::func2()
{
printf("func2\n");
}
在这个例子中, func1 是一个被隐式声明为 inline 的成员函数(定义在类定义内部的成员函数,则会被隐式地视为内联)。 func2 是一个被声明为inline的成员函数。当内联函数被调用时,编译器会尝试将内联函数的代码直接插入到调用点,而不是执行一个函数调用。
需要注意的是,内联是一种建议给编译器的优化提示,而不是强制性的。编译器可以选择忽略内联声明,特别是在以下情况下:
(1)如果函数体太大,编译器可能决定不进行内联
(2)如果函数有递归调用,它通常不会被内联
(3)如果内联会导致代码膨胀并可能影响性能,编译器可能会选择不进行内联
5 友元
友元( Friend )是一个特殊的关系,它允许一个类或函数访问另一个类的私有( private )成员或受保护( protected )成员。当一个类或函数被声明为另一个类的友元时,它就能够像该类的成员函数一样访问其所有成员。
友元可以是另一个类,也可以是一个函数。类友元是指一个类被声明为另一个类的友元。这种关系不是相互的,也不是传递的,必须显式地在类定义中声明。
5.1 友元的基本使用
类友元的声明通常在类的私有部分或公有部分进行,使用 friend 关键字。如下为样例代码:
#include <iostream>
class MyClass
{
// 声明一个友元类
friend class MyFriendClass;
public:
MyClass() : m_val(0){}
// 成员函数:设置值
void setVal(int val) {
m_val = val;
}
// 成员函数:获取值
int getVal() const { return m_val; }
private:
int m_val;
};
// 友元类
class MyFriendClass {
public:
void printVal(const MyClass& obj) {
// 友元类可以访问 MyClass 类型对象的私有成员
std::cout << "val = " << obj.m_val << std::endl;
}
// 注意:尽管 MyFriendClass 是 MyClass 的友元,但 MyClass 并不是 MyFriendClass 的友元
// 除非显式地在 MyFriendClass 中也声明 MyClass 为友元
};
int main() {
MyClass obj;
obj.setVal(2);
MyFriendClass friendObj;
friendObj.printVal(obj); // 可以访问 obj 的私有成员 m_val
return 0;
}
上面代码的输出为:
val = 2
在上面中, MyFriendClass 类被声明为 MyClass 类的友元。因此, MyFriendClass 类中的成员函数可以访问 MyClass 类的私有成员。然而,需要注意的是友元关系不是双向的: MyClass 类并不能访问 MyFriendClass 类的私有成员,除非 MyFriendClass 也显式地声明 MyClass 为它的友元。
注意:友元应该谨慎使用,因为它破坏了面向对象编程中的封装原则。
5.2 使用友元实现读写分离
设计一个温度计的模块,该模块包含 4 个类:
(1)温度值类:保存当前温度,并可以对历史温度数据做一些处理。
(1)温度传感器类:该类的主要作用是感应到环境的温度,将将其写入温度值类对象中。
(3)温度显示类:根据不同的温度值,分颜色显示。
(4)温度告警类:当温度高过一定值时,给出语音告警。
其中传感器类可以写入数值进入温度值类,而显示类以及温度告警类只能够从温度值类中读取数据,但是不能写入,这样读写分离的设计使得各个类的职责与权限明确,增强了代码开发的安全性(当代码量逐渐增大时,如果一个核心变量有很多更改源,对于调试以及维护就很不友好)。
具体代码如下:
#include <iostream>
class TemperatureSensor;
// 温度值类
class TemperatureValue
{
// 声明温度传感器类为友元类
friend class TemperatureSensor;
public:
double getVal()
{
return m_val;
}
private:
void setVal(double val)
{
m_val = val;
}
private:
double m_val; // 当前温度计值
};
// 温度传感器类
class TemperatureSensor
{
public:
TemperatureSensor(TemperatureValue* temperatureValue) :m_temperatureValue(temperatureValue) {}
public:
void setTemperatureVal(double val)
{
printf("update temperature value: %lf℃\n", val);
m_temperatureValue->setVal(val);
}
private:
TemperatureValue* m_temperatureValue;
};
// 温度显示类
class TemperatureDisplay
{
public:
TemperatureDisplay(TemperatureValue* temperatureValue) :m_temperatureValue(temperatureValue) {}
public:
void display()
{
printf("dislay temperature value: %lf℃\n", m_temperatureValue->getVal());
}
private:
TemperatureValue* m_temperatureValue;
};
// 温度告警类
class TemperatureAlarm
{
public:
TemperatureAlarm(TemperatureValue* temperatureValue) :m_temperatureValue(temperatureValue) {}
public:
void alarm()
{
printf("alarm temperature value: %lf℃\n", m_temperatureValue->getVal());
}
private:
TemperatureValue* m_temperatureValue;
};
int main()
{
TemperatureValue* temperatureValue = new TemperatureValue;
TemperatureSensor temperatureSensor(temperatureValue);
TemperatureDisplay temperatureDisplay(temperatureValue);
TemperatureAlarm temperatureAlarm(temperatureValue);
temperatureSensor.setTemperatureVal(36.2);
temperatureDisplay.display();
temperatureAlarm.alarm();
delete temperatureValue;
temperatureValue = nullptr;
return 0;
}
上面代码的输出为:
update temperature value: 36.200000℃
dislay temperature value: 36.200000℃
alarm temperature value: 36.200000℃
6 浅拷贝和深拷贝
浅拷贝( Shallow Copy )和深拷贝( Deep Copy )是对象复制时涉及的两个重要概念,主要是应用于当对象包含指针或其他动态分配的资源时。
6.1 浅拷贝( Shallow Copy )
浅拷贝是对象复制的一种形式,其中只复制对象本身和对象内部的指针,但不复制指针指向的实际数据。换句话说,浅拷贝只复制了对象的引用,而没有复制对象实际持有的数据。因此,原始对象和复制后的对象将共享同一块内存。如下为样例代码:
#include <iostream>
class MyClass
{
public:
MyClass(int value) {
m_val = new int(value);
}
~MyClass() {
delete m_val; // 该语句有可能会导致程序崩溃:原始对象或复制后的对象被销毁(调用析构函数),它们都会删除同一块内存
}
// 拷贝构造函数(浅拷贝)
MyClass(const MyClass& other)
{
m_val = other.m_val; // 只复制了指针,没有复制指针指向的数据
}
// 赋值运算符(浅拷贝)
MyClass& operator=(const MyClass& other)
{
if (this != &other)
{
m_val = other.m_val; // 只复制了指针,没有复制指针指向的数据
}
return *this;
}
public:
void setVal(int val)
{
*m_val = val;
}
void printVal()
{
printf("val = %d\n",*m_val);
}
private:
int* m_val;
};
int main() {
MyClass obj1(0);
MyClass obj2 = obj1;
obj2.setVal(1); // 通过 obj2 对象修改值,obj1 也会被改变
printf("obj1: ");
obj1.printVal();
printf("obj2: ");
obj2.printVal();
return 0;
}
上面代码的输出为:
obj1: val = 1
obj2: val = 1
在上面中,拷贝构造函数和赋值运算符只是复制了 m_val 指针,而没有复制指针指向的整数。因此,如果原始对象或复制后的对象被销毁(调用析构函数),它们都会尝试删除同一块内存,导致双重删除的错误。此外,如果修改其中一个对象的指针指向的数据,另一个对象的指针也会看到这些更改,因为它们都指向同一块内存(所以最后两个对象的输出都是 1 )。
6.2 深拷贝( Deep Copy )
深拷贝是对象复制的另一种形式,其中不仅复制对象本身和对象内部的指针,还复制指针指向的实际数据。这样,原始对象和复制后的对象将拥有独立的内存块。
使用深拷贝可以避免浅拷贝的问题,因为每个对象都持有自己的数据副本。
继续上面的例子,如果需要实现深拷贝,则要修改拷贝构造函数和赋值运算符来复制指针指向的数据:
#include <iostream>
class MyClass
{
public:
MyClass(int value) {
m_val = new int(value);
}
~MyClass() {
delete m_val;
}
// 拷贝构造函数(深拷贝)
MyClass(const MyClass& other)
{
m_val = new int(*(other.m_val));
}
// 赋值运算符(深拷贝)
MyClass& operator=(const MyClass& other)
{
if (this != &other)
{
delete m_val; // 先删除当前对象的数据
m_val = new int(*(other.m_val)); // 然后复制其他对象的数据
}
return *this;
}
public:
void setVal(int val)
{
*m_val = val;
}
void printVal()
{
printf("val = %d\n",*m_val);
}
private:
int* m_val;
};
int main() {
MyClass obj1(0);
MyClass obj2 = obj1;
obj2.setVal(1); // 通过 obj2 对象修改值,obj1 不会被改变
printf("obj1: ");
obj1.printVal();
printf("obj2: ");
obj2.printVal();
return 0;
}
上面代码的输出为:
obj1: val = 0
obj2: val = 1
在这个修改后的代码中,拷贝构造函数和赋值运算符都创建了一个新的整数,并复制了 obj1 对象中 m_val 指针指向的值。这样,每个对象都有自己独立的内存块,修改一个对象不会影响另一个对象。
深拷贝通常比浅拷贝更耗时和资源密集,因为它涉及到分配额外的内存和复制数据。然而,深拷贝提供了更好的数据完整性和安全性,特别是在涉及到多个对象共享同一份数据时。