C++作为一门多范式编程语言,其复杂性往往让初学者望而生畏。Scott Meyers在《Effective C++》中提出的"语言联邦"概念,为我们理解C++提供了绝佳的视角。这个联邦由四个主要子语言组成:C、Object-Oriented C++、Template C++和STL。
C子集构成了C++的基础层。在这个层面,程序员需要关注内存布局、指针运算和底层性能优化。例如,理解以下C风格代码的内存行为至关重要:
cpp复制int* arr = (int*)malloc(10 * sizeof(int));
arr[0] = 42; // 直接内存操作
free(arr);
**面向对象C++**引入了类、继承和多态等概念。这时我们需要关注封装、接口设计和虚函数机制。典型的面向对象代码可能如下:
cpp复制class Shape {
public:
virtual double area() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
double radius;
public:
explicit Circle(double r) : radius(r) {}
double area() const override { return 3.14 * radius * radius; }
};
**模板C++**开启了元编程的大门。模板不仅仅是泛型编程工具,还能实现编译期计算。考虑这个简单的模板示例:
cpp复制template<typename T>
T max(T a, T b) {
return a > b ? a : b;
}
// 特例化版本
template<>
const char* max<const char*>(const char* a, const char* b) {
return strcmp(a, b) > 0 ? a : b;
}
STL则提供了强大的算法和容器库。熟练使用STL能极大提升开发效率:
cpp复制std::vector<int> vec{1, 2, 3};
std::transform(vec.begin(), vec.end(), vec.begin(),
[](int x) { return x * 2; });
提示:在实际开发中,明确当前使用的C++子语言范式,能帮助选择最合适的编程风格和最佳实践。
宏定义是C时代的产物,在现代C++中应当谨慎使用。宏的主要问题在于它只是简单的文本替换,不参与类型系统和作用域规则。
常量定义应当优先使用constexpr:
cpp复制// 不好的做法
#define PI 3.14159
// 好的做法
constexpr double Pi = 3.14159;
类内常量可以使用static constexpr:
cpp复制class Buffer {
public:
static constexpr int DefaultSize = 1024;
char data[DefaultSize];
};
函数宏应当用内联函数替代:
cpp复制// 不好的做法
#define SQUARE(x) ((x)*(x))
// 好的做法
inline int square(int x) { return x * x; }
// 更好的做法(C++11起)
constexpr auto square(auto x) { return x * x; }
枚举值可以作为编译期常量使用:
cpp复制enum { ArraySize = 100 };
int arr[ArraySize];
注意:在头文件中定义常量时,确保使用inline变量(C++17起)避免多重定义问题:
cpp复制// header.h inline constexpr int GlobalConst = 42;
const是C++中强大的语义工具,它能帮助编译器发现潜在错误,同时提高代码可读性。
指针和引用的const用法需要特别注意:
cpp复制const int* p1; // 指向常量的指针
int const* p2; // 同上,不同写法
int* const p3; // 常量指针
const int* const p4; // 指向常量的常量指针
成员函数的const修饰表明不会修改对象状态:
cpp复制class TextBlock {
std::string text;
public:
const char& operator[](size_t pos) const {
return text[pos];
}
char& operator[](size_t pos) {
return const_cast<char&>(
static_cast<const TextBlock&>(*this)[pos]
);
}
};
逻辑常量性与物理常量性的区别:
cpp复制class CachedValue {
mutable bool cacheValid{false};
mutable int cachedResult;
int compute() const;
public:
int getValue() const {
if (!cacheValid) {
cachedResult = compute();
cacheValid = true;
}
return cachedResult;
}
};
提示:const应当成为默认选择,只有在确实需要修改时才去掉const限定。这种习惯能显著提高代码安全性。
C++中对象初始化是个复杂话题,未初始化变量是许多bug的根源。
内置类型必须手动初始化:
cpp复制int x = 0; // 好的做法
int y; // 不好的做法,值未定义
类成员应当使用成员初始化列表:
cpp复制class PhoneNumber { /*...*/ };
class ABEntry {
std::string name;
PhoneNumber phone;
int timesConsulted;
public:
ABEntry(const std::string& n, const PhoneNumber& p)
: name(n), phone(p), timesConsulted(0) {}
};
初始化顺序由声明顺序决定,与初始化列表顺序无关:
cpp复制class InitOrder {
int a;
int b;
public:
InitOrder(int val) : b(val), a(b) {} // 危险!a先于b初始化
};
静态对象的初始化问题:
cpp复制// 解决静态初始化顺序问题
static MyClass& getInstance() {
static MyClass instance; // C++11保证线程安全
return instance;
}
注意:在跨编译单元的静态对象初始化中,避免相互依赖。如果必须依赖,考虑使用"construct on first use"惯用法。
即使你不声明,编译器也会为类生成特殊成员函数。理解这些默认行为至关重要。
默认生成的函数包括:
何时不生成这些函数:
cpp复制class NoCopy {
NoCopy(const NoCopy&); // 只声明不实现
NoCopy& operator=(const NoCopy&);
public:
NoCopy() = default;
};
// 现代C++做法
class NonCopyable {
public:
NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
};
成员类型影响默认操作生成:
cpp复制class ResourceHolder {
std::unique_ptr<int> ptr; // 移动操作可用,拷贝操作被删除
};
提示:使用=default和=delete明确表达你的意图,比依赖编译器默认行为更清晰。
禁止拷贝是资源管理类的常见需求,有几种实现方式。
C++98方法:
cpp复制class NonCopyable98 {
protected:
NonCopyable98() {}
~NonCopyable98() {}
private:
NonCopyable98(const NonCopyable98&);
NonCopyable98& operator=(const NonCopyable98&);
};
现代C++方法更简洁:
cpp复制class NonCopyable {
public:
NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
};
继承boost::noncopyable也是一种选择:
cpp复制#include <boost/noncopyable.hpp>
class Derived : private boost::noncopyable {
// 自动禁用拷贝
};
注意:删除函数应当作为public成员,这样错误信息更清晰。如果放在private区,错误信息可能提到权限问题而非真正意图。
多态基类必须要有虚析构函数,这是C++的重要规则。
问题示例:
cpp复制class Base {
public:
~Base() {} // 非虚
};
class Derived : public Base {
std::string* str;
public:
Derived() : str(new std::string) {}
~Derived() { delete str; }
};
Base* pb = new Derived;
delete pb; // 未定义行为,Derived的析构函数不会被调用
正确做法:
cpp复制class Base {
public:
virtual ~Base() = default;
};
抽象类通常需要虚析构函数:
cpp复制class AbstractBase {
public:
virtual ~AbstractBase() = 0;
};
AbstractBase::~AbstractBase() = default;
提示:如果一个类有任何虚函数,它就应该有虚析构函数。这是良好的设计习惯。
析构函数中的异常可能导致程序直接终止,必须谨慎处理。
危险示例:
cpp复制class DBConn {
public:
~DBConn() {
db.close(); // 可能抛出异常
}
private:
Database db;
};
安全做法:
cpp复制class DBConn {
public:
~DBConn() noexcept {
try {
if (!closed) db.close();
} catch (...) {
// 记录日志或吞下异常
}
}
void close() { // 提供显式关闭接口
db.close();
closed = true;
}
private:
Database db;
bool closed{false};
};
异常安全保证:
注意:析构函数默认是noexcept的(C++11起),除非显式声明可能抛出异常。
构造和析构期间,虚函数机制与平时不同。
问题示例:
cpp复制class Transaction {
public:
Transaction() { log(); } // 调用的是基类版本
virtual void log() const;
};
class BuyTransaction : public Transaction {
public:
void log() const override;
};
BuyTransaction b; // 基类构造函数调用Transaction::log()
解决方案:
cpp复制class Transaction {
public:
explicit Transaction(const std::string& logInfo) {
log(logInfo); // 非虚调用
}
void log(const std::string& logInfo);
};
class BuyTransaction : public Transaction {
public:
BuyTransaction(params)
: Transaction(createLogString(params)) {}
private:
static std::string createLogString(params);
};
提示:在构造/析构期间,对象类型被视为当前构造阶段的类类型,虚函数不会下降到派生类实现。
赋值运算符应当返回左值引用以支持连锁赋值。
标准形式:
cpp复制class Widget {
public:
Widget& operator=(const Widget& rhs) {
if (this != &rhs) {
// 赋值逻辑
}
return *this;
}
};
支持连锁赋值:
cpp复制a = b = c; // 等价于a.operator=(b.operator=(c))
移动赋值运算符同样适用:
cpp复制Widget& operator=(Widget&& rhs) noexcept {
if (this != &rhs) {
// 移动逻辑
}
return *this;
}
注意:返回*this的引用是惯例,使自定义类型行为与内置类型一致。
自我赋值看起来罕见,但实际上可能以隐蔽方式发生。
危险实现:
cpp复制Widget& operator=(const Widget& rhs) {
delete pb; // 释放当前资源
pb = new Bitmap(*rhs.pb); // 如果rhs == *this,这里使用已删除对象
return *this;
}
解决方案1:身份测试
cpp复制Widget& operator=(const Widget& rhs) {
if (this == &rhs) return *this;
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
解决方案2:copy-and-swap
cpp复制Widget& operator=(Widget rhs) { // 按值传递,自动拷贝
swap(rhs); // 交换内容
return *this; // rhs析构会清理旧数据
}
提示:copy-and-swap惯用法不仅处理自我赋值,还提供强异常安全保证。
拷贝操作必须考虑基类部分和所有成员变量。
派生类拷贝构造函数:
cpp复制class Derived : public Base {
int derivedMember;
public:
Derived(const Derived& rhs)
: Base(rhs), // 拷贝基类部分
derivedMember(rhs.derivedMember) {}
};
派生类赋值运算符:
cpp复制Derived& operator=(const Derived& rhs) {
Base::operator=(rhs); // 赋值基类部分
derivedMember = rhs.derivedMember;
return *this;
}
常见错误:
cpp复制Derived& operator=(const Derived& rhs) {
if (this == &rhs) return *this;
derivedMember = rhs.derivedMember;
// 忘记赋值基类部分!
return *this;
}
注意:拷贝构造函数和赋值运算符不应相互调用。如果需要共享逻辑,创建第三个函数供两者调用。