c++类(class)特性
C++是一种多范式编程语言,它支持面向过程编程、面向对象编程(OOP)和泛型编程。在面向对象编程方面,类是C++的核心特性之一。类不仅是一种数据封装的方式,还包含了继承、多态等概念。下面是C++类的一些基本特性:
1. 封装(Encapsulation)
封装是面向对象编程的基石之一,它允许将数据(属性)和与数据相关的方法(行为)组合成一个整体——即类。通过封装,可以隐藏类的内部细节,只对外公开需要的接口。这样做的好处是提高了代码的安全性和易用性。
class BankAccount
{
private:
double balance; // 私有属性,外部访问受限
public:
void deposit(double amount)
{
balance += amount; // 公共方法,可以被外部调用
}
double getBalance()
{
return balance; // 提供公共接口访问私有数据
}
};
2. 继承(Inheritance)
继承允许一个类(派生类)继承另一个类(基类)的属性和方法。这可以提高代码的复用性,并可以建立类之间的层次关系。继承是实现多态性的关键。
class Base
{
public:
void show()
{
std::cout << "Base class show function called." << std::endl;
}
};
class Derived : public Base
{
public:
void show()
{
std::cout << "Derived class show function called." << std::endl;
}
};
3. 多态(Polymorphism)
多态性意味着调用成员函数时会根据调用函数的对象的实际类型来执行不同的函数。这通常通过虚函数(使用virtual
关键字定义的函数)来实现。
class Animal
{
public:
virtual void speak() // 虚函数
{
std::cout << "Some animal makes a sound." << std::endl;
}
};
class Dog : public Animal
{
public:
void speak() override // 重写基类的函数
{
std::cout << "Dog barks." << std::endl;
}
};
void makeAnimalSound(Animal& animal)
{
animal.speak(); // 在运行时确定调用哪个类的speak()
}
4. 抽象(Abstraction)
抽象是通过使用抽象类和纯虚函数来实现的。抽象类至少包含一个纯虚函数,主要用于作为基类。抽象类不能被实例化,只能被继承。
class AbstractEmployee
{
public:
virtual void askForPromotion() = 0; // 纯虚函数
};
class Employee : public AbstractEmployee
{
public:
void askForPromotion() override
{
std::cout << "Asking for promotion." << std::endl;
}
};
5. 构造函数和析构函数
每个类可以有一个或多个构造函数,用于初始化对象的状态。析构函数用于在对象生命周期结束时进行清理工作。
class Example
{
public:
Example()
{
std::cout << "Constructor called." << std::endl;
}
~Example()
{
std::cout << "Destructor called." << std::endl;
}
};
进阶之空类
在 C++ 中,一个空类是指没有声明任何数据成员和成员函数的类。尽管这样的类在表面上看似无用,但它们在某些场景下仍然有其特定的用途和一些有趣的属性。以下是 C++ 空类的一些主要特点和考虑事项:
1. 默认成员函数
C++ 标准规定,即使类体为空,编译器也会为类自动生成一些成员函数,除非显式禁止。这些包括:
- 默认构造函数:如果没有其他构造函数被定义,则编译器会提供一个默认的构造函数。
- 拷贝构造函数:用于通过同类型的另一个对象来初始化新对象。
- 拷贝赋值运算符:用于将一个类的实例赋值给另一个同类型的实例。
- 析构函数:在对象生命周期结束时调用,用于执行清理工作。
- 移动构造函数(C++11 及以后):允许资源的转移而非拷贝。
- 移动赋值运算符(C++11 及以后):也是用于资源的转移。
2. 对象大小
在 C++ 中,空类的实例不是零大小。为了确保每个对象都有独一无二的地址,C++ 标准规定即便类为空,其对象的大小至少为 1 字节。这意味着,即使是空类,创建其对象也会在内存中占用至少 1 字节的空间。
class Empty {};
int main()
{
Empty e1, e2;
std::cout << "Size of empty class: " << sizeof(Empty) << std::endl; // 输出 1
std::cout << "Address of e1: " << &e1 << std::endl;
std::cout << "Address of e2: " << &e2 << std::endl;
return 0;
}
3. 继承和多重继承
空类常常用作基类,尤其是在使用多重继承时,可以作为标记用途。由于空基类优化(Empty Base Optimization, EBO),在多数编译器实现中,如果一个类从一个或多个空基类继承,这些基类通常不会增加派生类的大小。这是一种避免不必要的内存占用的优化技术。
4. 作为类型安全的标记
空类可以在编程中用作类型安全的标记。例如,它们可以在重载解析中用来区分不同的函数重载版本,或者作为策略类的占位符,在模板元编程中非常有用。
5. 接口和多态
虽然空类本身不包含任何功能,但如果在空类中声明虚函数(包括纯虚函数),它就可以作为接口使用。这允许派生类实现具体的功能,同时利用多态性。
尽管空类看起来可能没有实际内容,但在 C++ 中,它们仍然是定义行为、接口以及类型系统的有用工具。理解空类的这些特性可以帮助开发者更好地利用 C++ 的面向对象特性来设计软件。
进阶之虚基类/虚继承
在 C++ 中,虚基类主要用于解决多重继承时可能出现的菱形继承问题(Diamond Problem)。当两个(或多个)派生类继承自同一个基类,而又有一个类继承自这些派生类时,基类的单一实例在派生类中会被重复包含,导致资源浪费和潜在的不一致性。使用虚基类可以确保在多重继承结构中,基类只有一个共享实例,不管它被继承了多少次。
虚基类的主要特点
1. 解决菱形继承问题
在多重继承中,如果一个类作为虚基类,那么无论这个基类被间接继承多少次,派生类中都只会有一个基类的实例。
2. 构造顺序
虚基类的构造顺序与非虚基类不同。虚基类总是在任何非虚基类之前构造,这是为了确保当派生类访问虚基类的成员时,这些成员已经被初始化。在构造派生类对象时,虚基类的构造函数由最派生的类调用,不是由中间派生类调用。
3. 存储布局
虚基类的存储布局也与非虚基类不同。编译器通常使用特殊的技术来管理虚基类,确保所有派生自该虚基类的类共享其数据成员的单一实例。
示例:使用虚基类解决菱形继承问题
假设有一个基类 Base
,两个派生类 Derived1
和 Derived2
从 Base
继承,再有一个类 MostDerived
从 Derived1
和 Derived2
继承:
#include <iostream>
class Base {
public:
int value;
Base() : value(0) {
std::cout << "Base constructor\n";
}
};
class Derived1 : virtual public Base {
public:
Derived1() {
std::cout << "Derived1 constructor\n";
}
};
class Derived2 : virtual public Base {
public:
Derived2() {
std::cout << "Derived2 constructor\n";
}
};
class MostDerived : public Derived1, public Derived2 {
public:
MostDerived() : Base() { // 明确调用 Base 的构造函数
std::cout << "MostDerived constructor\n";
}
};
int main() {
MostDerived md;
md.value = 1; // 没有歧义,因为只有一个 Base 实例
std::cout << "Base value in MostDerived: " << md.value << std::endl;
return 0;
}
在这个例子中,Base
被声明为虚基类,因此 MostDerived
类中只包含一个 Base
类的实例。不管 Base
被间接继承了多少次,都只存在一个共享的 Base
实例。
注意事项
- 使用虚基类可能会引入额外的复杂性,如需要更多的内存来管理虚继承的额外信息,以及潜在的性能开销。
- 在设计类层次结构时,仅当真正需要解决菱形继承问题时才应考虑使用虚基类。
- 理解和正确使用虚基类需要对 C++ 的继承机制有深刻的理解。
虚基类是 C++ 多重继承机制中一个强大但需要谨慎使用的特性,正确使用可以使代码更加灵活和健壮。
进阶之单例
在 C++ 中,单例模式是一种确保类只有一个实例,并提供一个全局访问点来获取该实例的设计模式。单例模式常用于控制对某些共享资源的访问,如配置文件处理器或数据库连接管理器。
C++ 单例类的实现
实现单例模式通常涉及以下几个步骤:
- 私有静态实例:类自己持有它的唯一实例。
- 私有构造函数:防止外部使用
new
或其他方式创建类的实例。 - 公共的静态方法:用于获取单例对象的引用或指针。
- 删除复制构造函数和赋值操作符:防止复制和赋值实例。
示例代码
以下是一个简单的单例类实现示例:
#include <iostream>
class Singleton {
private:
static Singleton* instance; // 静态成员,存储单例实例的地址
Singleton() {} // 私有构造函数
public:
// 删除复制构造函数和赋值操作符
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 静态方法,用于访问实例
static Singleton* getInstance() {
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
void doSomething() {
std::cout << "Doing something." << std::endl;
}
};
// 初始化静态成员
Singleton* Singleton::instance = nullptr;
int main() {
Singleton* singleton = Singleton::getInstance();
singleton->doSomething();
// 试图获取另一个实例
Singleton* anotherSingleton = Singleton::getInstance();
anotherSingleton->doSomething();
return 0;
}
重要注意事项
- 线程安全:在多线程环境中,单例模式的实现需要考虑线程安全问题。上述示例在多线程环境下不是线程安全的,因为两个线程可能同时检查
instance == nullptr
并尝试创建实例。解决这个问题通常需要使用互斥锁(mutex)或其他同步机制。 - 内存泄漏:单例实例通常在应用程序的整个生命周期内存在,但如果需要释放单例资源,应考虑在适当的时候正确地删除它。
- 静态局部变量实现:为了简化代码并提高线程安全,可以使用静态局部变量来实现单例模式,这样利用了局部静态变量的线程安全初始化特性(C++11 以后)。
使用静态局部变量的单例实现
class Singleton {
private:
Singleton() {}
~Singleton() {}
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
void doSomething() {
std::cout << "Doing something." << std::endl;
}
};
这种实现方式简单,且自动为多线程环境提供了线程安全的初始化,而无需显式使用互斥锁。
未完待续~
开发实战进阶之类class【c++】二