1. C++ 类与对象全面解析
作为C++面向对象编程的核心概念,类和对象是每个C++开发者必须掌握的基础知识。本文将系统性地介绍C++中类和对象的各个方面,从基础概念到高级特性,帮助读者全面理解这一重要主题。
1.1 类的基本定义与结构
在C++中,类是一种用户自定义的数据类型,它将数据和操作这些数据的函数封装在一起。类的定义使用class关键字,基本语法如下:
cpp复制class ClassName {
// 成员变量和成员函数
};
以栈(Stack)为例,我们可以这样定义一个简单的栈类:
cpp复制class Stack {
private:
int* a; // 存储栈元素的数组
int top; // 栈顶指针
int capacity; // 栈容量
public:
// 成员函数声明
void push(int value);
int pop();
bool isEmpty() const;
};
类中的变量称为成员变量或属性,函数称为成员函数或方法。成员变量通常使用特定命名约定,如以下划线_或m开头,以区别于普通变量:
cpp复制class Example {
int regularVar; // 普通命名
int _privateVar; // 下划线前缀
int m_memberVar; // m前缀
};
注意:虽然C++中的
struct也可以定义类(并且默认访问权限是public),但通常建议使用class来定义类,以明确封装意图。
1.2 访问控制与封装
C++通过访问限定符实现封装,这是面向对象编程的三大特性之一(封装、继承、多态)。访问限定符包括:
public:公有成员,可以在类外部直接访问private:私有成员,只能在类内部访问protected:保护成员,类似于private,但在继承中有特殊含义
访问限定符的作用域从声明处开始,直到下一个访问限定符或类定义结束:
cpp复制class AccessExample {
public: // 从这里开始是public区域
void publicFunc();
private: // 从这里开始是private区域
int privateVar;
protected: // 从这里开始是protected区域
void protectedFunc();
}; // 类定义结束
默认情况下,class中的成员是private的,而struct中的成员是public的。良好的封装实践通常将成员变量设为private,只通过public成员函数提供访问接口。
1.3 类的作用域与成员定义
类定义了一个新的作用域,类的所有成员都在这个作用域内。在类外部定义成员函数时,需要使用作用域解析运算符::指明所属类:
cpp复制class ScopeExample {
public:
void memberFunc(); // 声明
};
// 在类外部定义成员函数
void ScopeExample::memberFunc() {
// 函数实现
}
类作用域影响编译器的名称查找规则。当编译器遇到一个名称时,会先在当前作用域查找,然后在外层作用域查找。使用::可以明确告诉编译器在哪个类的作用域中查找该名称。
2. 类的实例化与对象使用
2.1 对象实例化概念
类是对对象的抽象描述,而对象是类的具体实例。实例化是指在内存中创建类的具体对象的过程。类本身不占用存储空间,只有实例化出的对象才占用实际内存。
cpp复制class Point {
public:
int x;
int y;
};
int main() {
Point p1; // 实例化一个Point对象
Point p2; // 实例化另一个Point对象
return 0;
}
实例化过程类似于基本类型的变量声明。实际上,类可以看作用户自定义的类型,而对象就是用这种类型创建的变量。
2.2 对象的内存布局与大小
对象的大小由其成员变量决定,不包括成员函数。成员函数被放在代码区,由所有对象共享。计算对象大小时需要考虑内存对齐规则:
- 第一个成员在偏移量为0的位置
- 后续成员对齐到其类型大小的整数倍地址
- 结构体/类的总大小是其最大对齐数的整数倍
考虑以下PersonBirthday类:
cpp复制class PersonBirthday {
private:
char name[15]; // 15字节
int year; // 4字节
int month; // 4字节
int day; // 4字节
};
其内存布局如下(假设4字节对齐):
- name[15]:偏移0-14
- 填充1字节(使year对齐到4的倍数)
- year:偏移16-19
- month:偏移20-23
- day:偏移24-27
- 总大小:28字节
对于空类(没有成员变量),其大小至少为1字节,用于标识对象的存在。
2.3 this指针原理
this指针是C++中的一个隐式指针,指向当前对象的地址。每个成员函数(非static)都隐含一个this指针参数,编译器在调用成员函数时自动传递当前对象的地址。
cpp复制class ThisExample {
int value;
public:
void setValue(int value) {
this->value = value; // 使用this区分成员变量和参数
}
};
编译器会将上述代码转换为类似下面的形式:
cpp复制void setValue(ThisExample* this, int value) {
this->value = value;
}
this指针通常存储在寄存器(如ECX)或栈中,具体取决于编译器和调用约定。
3. 类的默认成员函数
3.1 构造函数与初始化
构造函数是特殊的成员函数,用于初始化对象。它具有以下特点:
- 与类同名
- 无返回值(连void都不写)
- 可以重载
- 对象创建时自动调用
cpp复制class ConstructorExample {
int value;
public:
ConstructorExample() : value(0) {} // 默认构造函数
ConstructorExample(int v) : value(v) {} // 带参构造函数
};
如果类没有定义任何构造函数,编译器会生成一个默认的无参构造函数。但有以下例外情况:
- 类中有const成员或引用成员
- 类成员没有默认构造函数
- 基类没有默认构造函数
C++11允许在成员声明时提供默认值:
cpp复制class DefaultValues {
int x = 0; // 默认值
std::string s = "default";
};
3.2 析构函数与资源释放
析构函数用于清理对象资源,特点包括:
- 名为
~ClassName - 无参数无返回值
- 不能重载
- 对象销毁时自动调用
cpp复制class DestructorExample {
int* data;
public:
DestructorExample(size_t size) : data(new int[size]) {}
~DestructorExample() { delete[] data; } // 释放动态内存
};
对于管理资源的类(如动态内存、文件句柄等),必须自定义析构函数。大多数简单类可以使用编译器生成的默认析构函数。
3.3 拷贝控制:拷贝构造与赋值
拷贝构造函数用于用一个已存在对象初始化新对象:
cpp复制class CopyConstructorExample {
int* data;
size_t size;
public:
// 拷贝构造函数
CopyConstructorExample(const CopyConstructorExample& other)
: size(other.size), data(new int[other.size]) {
std::copy(other.data, other.data + size, data);
}
};
赋值运算符重载用于两个已存在对象间的赋值:
cpp复制class AssignmentExample {
int* data;
size_t size;
public:
// 赋值运算符
AssignmentExample& operator=(const AssignmentExample& other) {
if (this != &other) { // 防止自赋值
delete[] data; // 释放原有资源
size = other.size;
data = new int[size];
std::copy(other.data, other.data + size, data);
}
return *this; // 支持链式赋值
}
};
遵循"三法则":如果一个类需要自定义析构函数、拷贝构造函数或赋值运算符,那么它通常需要全部三个。
4. 运算符重载与特殊成员函数
4.1 运算符重载基础
运算符重载允许为用户定义类型赋予运算符的特殊含义。重载形式有两种:
- ���为成员函数(隐含this参数)
- 作为全局函数(所有参数显式传递)
cpp复制class Vector {
double x, y;
public:
// 成员函数形式重载+
Vector operator+(const Vector& other) const {
return Vector(x + other.x, y + other.y);
}
// 全局函数形式重载<<(通常声明为友元)
friend std::ostream& operator<<(std::ostream& os, const Vector& v);
};
// 全局函数实现
std::ostream& operator<<(std::ostream& os, const Vector& v) {
return os << "(" << v.x << ", " << v.y << ")";
}
不能重载的运算符:., .*, ::, ?:, sizeof。
4.2 特殊成员函数
除了构造函数、析构函数和拷贝控制成员,类还有以下特殊成员函数:
- 取地址运算符重载:
cpp复制class AddressExample {
public:
AddressExample* operator&() { return this; }
const AddressExample* operator&() const { return this; }
};
- 移动构造函数和移动赋值运算符(C++11引入):
cpp复制class MoveExample {
int* data;
public:
// 移动构造函数
MoveExample(MoveExample&& other) noexcept
: data(other.data) {
other.data = nullptr;
}
// 移动赋值运算符
MoveExample& operator=(MoveExample&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
other.data = nullptr;
}
return *this;
}
};
移动语义允许资源所有权的转移而非拷贝,能显著提高性能。
5. 高级类特性
5.1 static成员
static成员属于类而非特定对象,被所有对象共享:
cpp复制class StaticExample {
static int count; // 声明
public:
StaticExample() { ++count; }
~StaticExample() { --count; }
static int getCount() { return count; }
};
int StaticExample::count = 0; // 定义并初始化
static成员函数没有this指针,只能访问static成员变量。
5.2 友元关系
友元(friend)打破了封装,允许特定函数或类访问私有成员:
cpp复制class FriendExample {
int secret;
public:
friend void friendFunction(FriendExample& f);
friend class FriendClass;
};
void friendFunction(FriendExample& f) {
f.secret = 42; // 可以访问私有成员
}
class FriendClass {
public:
void modify(FriendExample& f) {
f.secret = 100; // 可以访问私有成员
}
};
友元关系是单向的,不传递,应谨慎使用。
5.3 内部类
内部类是在另一个类内部定义的类,可以访问外部类的私有成员:
cpp复制class Outer {
int outerValue;
class Inner {
public:
void accessOuter(Outer& o) {
o.outerValue = 10; // 可以访问外部类私有成员
}
};
};
内部类常用于实现细节隐藏或紧密相关的辅助类。
5.4 匿名对象
匿名对象是没有名字的临时对象,生命周期通常只有创建它的表达式:
cpp复制class Temp {
public:
Temp() { std::cout << "Created\n"; }
~Temp() { std::cout << "Destroyed\n"; }
};
void useTemp(const Temp& t) {}
int main() {
useTemp(Temp()); // 创建匿名对象作为参数
// 匿名对象在此处销毁
return 0;
}
输出:
code复制Created
Destroyed
6. 实战示例:栈类实现
下面是一个完整的栈类实现,展示了前面讨论的许多概念:
cpp复制// Stack.h
#pragma once
#include <cassert>
#include <cstring>
#include <iostream>
class Stack {
private:
char* _arr; // 存储元素
size_t _top; // 栈顶指针
size_t _capacity; // 栈容量
void resize(); // 扩容辅助函数
public:
// 迭代器支持
typedef char* iterator;
typedef const char* const_iterator;
iterator begin() { return _arr; }
iterator end() { return _arr + _top; }
// 构造函数
explicit Stack(size_t capacity = 4);
// 拷贝控制
Stack(const Stack& other);
Stack& operator=(const Stack& other);
~Stack();
// 栈操作
void push(char c);
void pop();
char top() const;
bool empty() const;
size_t size() const;
// 友元IO操作
friend std::ostream& operator<<(std::ostream& os, const Stack& s);
friend std::istream& operator>>(std::istream& is, Stack& s);
};
// Stack.cpp
#include "Stack.h"
Stack::Stack(size_t capacity)
: _arr(new char[capacity]), _top(0), _capacity(capacity) {}
Stack::Stack(const Stack& other)
: _arr(new char[other._capacity]), _top(other._top), _capacity(other._capacity) {
std::copy(other._arr, other._arr + _top, _arr);
}
Stack& Stack::operator=(const Stack& other) {
if (this != &other) {
delete[] _arr;
_capacity = other._capacity;
_top = other._top;
_arr = new char[_capacity];
std::copy(other._arr, other._arr + _top, _arr);
}
return *this;
}
Stack::~Stack() {
delete[] _arr;
}
void Stack::resize() {
size_t newCapacity = _capacity * 1.5;
if (newCapacity <= _capacity) newCapacity = _capacity + 1;
char* newArr = new char[newCapacity];
std::copy(_arr, _arr + _top, newArr);
delete[] _arr;
_arr = newArr;
_capacity = newCapacity;
}
void Stack::push(char c) {
if (_top == _capacity) resize();
_arr[_top++] = c;
}
void Stack::pop() {
assert(!empty());
--_top;
}
char Stack::top() const {
assert(!empty());
return _arr[_top - 1];
}
bool Stack::empty() const {
return _top == 0;
}
size_t Stack::size() const {
return _top;
}
std::ostream& operator<<(std::ostream& os, const Stack& s) {
for (size_t i = 0; i < s._top; ++i) {
os << s._arr[i];
}
return os;
}
std::istream& operator>>(std::istream& is, Stack& s) {
std::string input;
is >> input;
Stack temp(input.size());
for (char c : input) {
temp.push(c);
}
s = temp;
return is;
}
这个栈实现展示了:
- 动态内存管理(构造函数、析构函数、拷贝控制)
- 运算符重载(<<和>>)
- 迭代器支持(begin/end)
- 异常安全(通过RAII)
- 封装与接口设计
7. 最佳实践与常见问题
7.1 类设计原则
- 单一职责原则:一个类应该只有一个引起它变化的原因
- 开放封闭原则:对扩展开放,对修改封闭
- Liskov替换原则:派生类应该能够替换基类而不影响程序正确性
- 接口隔离原则:客户端不应被迫依赖它们不使用的接口
- 依赖倒置原则:依赖抽象而非具体实现
7.2 常见错误与解决方案
-
浅拷贝问题:
- 错误:默认拷贝构造函数和赋值运算符执行浅拷贝
- 解决:对于管理资源的类,实现深拷贝
-
自赋值问题:
cpp复制// 错误的赋值运算符 MyClass& operator=(const MyClass& other) { delete[] data; // 如果this == &other,这里就删除了other的数据 data = new int[other.size]; // ... }- 解决:添加自赋值检查
cpp复制MyClass& operator=(const MyClass& other) { if (this != &other) { // 执行赋值 } return *this; } -
异常安全问题:
- 使用RAII(Resource Acquisition Is Initialization)模式
- 在修改对象状态前确保���作不会失败
-
const正确性:
- 将不修改对象状态的成员函数声明为const
- 正确使用const引用传递参数
7.3 性能优化技巧
- 移动语义:对于临时对象,使用移动而非拷贝
- 返回值优化(RVO):依赖编译器优化避免不必要的拷贝
- 内联小函数:将简单频繁调用的函数内联
- 避免虚函数过度使用:虚函数调用有额外开销
- 缓存友好设计:合理安排成员变量顺序,提高缓存命中率
8. C++与Java类机制对比
虽然C++和Java都支持面向对象编程,但它们的类机制有显著差异:
| 特性 | C++ | Java |
|---|---|---|
| 内存管理 | 手动/RAII | 自动垃圾回收 |
| 多重继承 | 支持 | 不支持(仅支持接口多重继承) |
| 访问控制 | public/private/protected | 类似,还有包访问权限 |
| 默认成员函数 | 有(构造、析构等) | 有限(只有默认构造) |
| 运算符重载 | 支持 | 不支持 |
| 值语义 | 支持(对象可以放在栈上) | 只有引用语义(对象都在堆上) |
| 模板 | 强大(图灵完备) | 泛型(类型擦除实现) |
| 运行时类型信息 | 有限(需要显式启用RTTI) | 全面支持反射 |
理解这些差异有助于在不同语言间转换思维,编写更符合语言特性的代码。
