1. C++类与对象基础概念
在C++中,类(class)是面向对象编程的核心概念,它允许我们将数据(属性)和操作数据的方法(成员函数)封装在一起。与C语言中的结构体相比,C++的类提供了更强大的封装性和抽象能力。
1.1 类的基本定义格式
定义一个类的基本语法如下:
cpp复制class ClassName {
// 访问限定符(public/protected/private)
// 成员变量和成员函数
};
例如,我们定义一个简单的日期类:
cpp复制class Date {
public:
void Init(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void Print() {
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
注意:类定义结束时后面的分号不能省略。类体中的内容称为类的成员,包括成员变量(属性)和成员函数(方法)。
1.2 C++中struct的升级
C++对C语言的struct进行了升级,使其也具有类的特性:
- struct内部可以定义函数
- struct名称可以直接作为类型名使用(不再需要typedef)
cpp复制// C语言风格
typedef struct ListNodeC {
struct ListNodeC* next;
int val;
} LTNode;
// C++风格
struct ListNodeCPP {
void Init(int x) {
next = nullptr;
val = x;
}
ListNodeCPP* next;
int val;
};
1.3 访问限定符
C++通过访问限定符实现封装:
public:修饰的成员在类外可以直接访问protected和private:修饰的成员在类外不能直接访问(protected和private的区别在继承中体现)
访问限定符的作用域从它出现的位置开始,直到下一个访问限定符出现为止。如果没有下一个访问限定符,作用域就到类定义结束。
重要区别:class默认访问权限是private,而struct默认是public。
2. 类的实例化
2.1 实例化概念
用类类型创建对象的过程称为类的实例化。类本身只是一个"蓝图",只有实例化出对象时才会分配实际的内存空间。
cpp复制class Date {
// 类定义...
};
int main() {
Date d1; // 实例化对象d1
Date d2; // 实例化对象d2
d1.Init(2024, 3, 31);
d1.Print();
return 0;
}
2.2 对象的内存布局
类实例化出的每个对象都有独立的数据空间(成员变量),但成员函数是所有对象共享的(存储在代码段)。
对象的大小计算遵循内存对齐规则:
- 第一个成员在与结构体偏移量为0的地址处
- 其他成员变量要对齐到某个数字(对齐数)的整数倍地址处
- 对齐数 = min(编译器默认对齐数, 成员大小)
- VS中默认对齐数为8
- 结构体总大小为最大对齐数的整数倍
- 嵌套结构体的情况需要特殊处理
注意:成员函数不占用对象的内存空间。空类的大小为1字节,纯粹是为了标识对象存在。
3. this指针
3.1 this指针的原理
编译器在编译成员函数时,会自动在形参第一个位置添加一个当前类类型的指针,称为this指针。成员函数中访问成员变量实际上都是通过this指针访问的。
cpp复制void Init(int year, int month, int day) {
this->_year = year; // 等同于 _year = year;
this->_month = month;
this->_day = day;
}
3.2 this指针的特性
- this指针不能在形参和实参位置显式写出,但可以在函数体内显式使用
- this指针本质是成员函数的隐含参数
- this指针一般存储在栈或寄存器中
cpp复制class A {
public:
void Print() {
cout << "A::Print()" << endl;
// 如果访问成员变量则会崩溃,因为this为nullptr
}
private:
int _a;
};
int main() {
A* p = nullptr;
p->Print(); // 可以运行,因为不访问成员变量
return 0;
}
4. 类的默认成员函数
如果一个类没有显式定义以下成员函数,编译器会自动生成默认版本:
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值运算符重载
- 取地址运算符重载
- const取地址运算符重载
5. 构造函数详解
5.1 构造函数的特点
构造函数是特殊的成员函数,主要任务是在对象实例化时初始化对象:
- 函数名与类名相同
- 无返回值(连void都不写)
- 对象实例化时自动调用
- 可以重载
- 如果没有显式定义,编译器会自动生成一个无参的默认构造函数
5.2 默认构造函数
默认构造函数包括:
- 无参构造函数
- 全缺省构造函数
- 编译器自动生成的构造函数
这三种构造函数有且只能存在一个,不能同时存在。
cpp复制class Date {
public:
// 1. 无参构造函数
Date() {
_year = 1;
_month = 1;
_day = 1;
}
// 2. 带参构造函数
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
// 3. 全缺省构造函数
Date(int year = 1, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
};
注意:通过无参构造函数创建对象时,对象后面不要跟括号,否则会被认为是函数声明。
5.3 构造函数的初始化列表
构造函数除了在函数体内赋值外,还可以使用初始化列表:
cpp复制class Date {
public:
Date(int year, int month, int day)
: _year(year) // 初始化列表
, _month(month)
, _day(day)
{}
};
初始化列表的特点:
- 每个成员变量在初始化列表中只能出现一次
- 引用成员、const成员、没有默认构造的类类型成员必须在初始化列表中初始化
- C++11支持在成员变量声明时给缺省值
- 初始化顺序与成员变量声明顺序一致(与初始化列表中的顺序无关)
6. 析构函数详解
6.1 析构函数的特点
析构函数与构造函数功能相反,用于对象销毁时清理资源:
- 函数名是在类名前加~
- 无参数无返回值
- 一个类只能有一个析构函数
- 对象生命周期结束时自动调用
- 如果没有显式定义,编译器会自动生成默认析构函数
cpp复制class Stack {
public:
Stack(int n = 4) {
_a = (int*)malloc(sizeof(int) * n);
_capacity = n;
_top = 0;
}
~Stack() {
free(_a); // 释放动态分配的内存
_a = nullptr;
_top = _capacity = 0;
}
};
6.2 析构函数的调用规则
- 对于内置类型成员,默认析构函数不做处理
- 对于自定义类型成员,会调用其析构函数
- 局部对象的析构顺序与构造顺序相反(后定义先析构)
- 有资源申请时一定要自己写析构函数,否则会造成资源泄漏
7. 拷贝构造函数
7.1 拷贝构造的定义
拷贝构造函数是一种特殊的构造函数,用于创建一个对象的副本:
cpp复制class Stack {
public:
Stack(const Stack& st) { // 拷贝构造
_a = (int*)malloc(sizeof(int) * st._capacity);
memcpy(_a, st._a, sizeof(int) * st._top);
_top = st._top;
_capacity = st._capacity;
}
};
7.2 拷贝构造的特点
- 是构造函数的一个重载形式
- 第一个参数必须是类类型对象的引用(否则会无限递归)
- 自定义类型对象进行拷贝时必须调用拷贝构造
- 如果没有显式定义,编译器会自动生成一个浅拷贝的版本
深拷贝 vs 浅拷贝:
- 浅拷贝:简单按字节复制,可能导致多个对象共享同一资源
- 深拷贝:为新对象独立分配资源,复制内容
8. 运算符重载
8.1 运算符重载的基本概念
运算符重载允许我们为类类型对象定义运算符的特殊含义:
cpp复制class Date {
public:
bool operator==(const Date& d) const {
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
Date& operator+=(int day) {
// 实现日期加天数逻辑
return *this;
}
};
8.2 运算符重载的规则
- 不能创建新的运算符
- 不能重载以下运算符:
.*::sizeof?:. - 重载运算符至少有一个类类型参数
- 作为成员函数时,第一个运算对象默认是this指针
- 重载运算符应保持原有语义
8.3 特殊运算符重载
-
前置++和后置++:
cpp复制// 前置++ Date& operator++() { *this += 1; return *this; } // 后置++ Date operator++(int) { Date tmp(*this); *this += 1; return tmp; } -
输入输出运算符:
cpp复制friend ostream& operator<<(ostream& out, const Date& d); friend istream& operator>>(istream& in, Date& d);
9. 特殊类成员
9.1 static成员
static成员属于类而不是对象:
- static成员变量必须在类外初始化
- static成员函数没有this指针
- 可以通过类名或对象访问static成员
cpp复制class A {
public:
static int GetCount() { return _count; }
private:
static int _count;
};
int A::_count = 0; // 类外初始化
9.2 友元
友元突破了类的封装,分为友元函数和友元类:
cpp复制class Date {
friend ostream& operator<<(ostream& out, const Date& d);
friend class Printer; // 友元类
};
友元的特点:
- 友元关系是单向的
- 友元关系不能传递
- 友元会破坏封装,应谨慎使用
9.3 内部类
定义在另一个类内部的类称为内部类:
- 内部类是一个独立的类
- 内部类默认是外部类的友元
- 外部类对象不包含内部类成员
cpp复制class Outer {
private:
static int _k;
int _h = 1;
public:
class Inner { // 内部类
public:
void foo(const Outer& o) {
cout << _k << endl; // 可以访问外部类静态成员
cout << o._h << endl; // 可以访问外部类私有成员
}
};
};
10. 对象拷贝的编译器优化
现代编译器会对对象拷贝进行优化:
- 连续构造+拷贝构造 → 优化为直接构造
- 传值返回时可能优化掉临时对象
- 不同编译器的优化策略可能不同
cpp复制A f2() {
A aa;
return aa; // 可能被优化
}
A aa2 = f2(); // 可能被优化为直接构造
11. 日期类完整实现
下面是一个完整的日期类实现示例:
cpp复制// Date.h
#pragma once
#include <iostream>
#include <assert.h>
class Date {
public:
int GetMonthDay(int year, int month) const;
void Print() const;
Date(int year = 1, int month = 1, int day = 1);
Date operator+(int day) const;
Date& operator+=(int day);
// 其他运算符重载...
friend std::ostream& operator<<(std::ostream& out, const Date& d);
friend std::istream& operator>>(std::istream& in, Date& d);
private:
int _year;
int _month;
int _day;
};
// Date.cpp
#include "Date.h"
int Date::GetMonthDay(int year, int month) const {
assert(month > 0 && month < 13);
static int monthDayArr[13] = { -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) {
return 29;
}
return monthDayArr[month];
}
Date::Date(int year, int month, int day)
: _year(year), _month(month), _day(day) {
assert(month > 0 && month < 13);
assert(day > 0 && day <= GetMonthDay(year, month));
}
Date& Date::operator+=(int day) {
if (day < 0) {
return *this -= (-day);
}
_day += day;
while (_day > GetMonthDay(_year, _month)) {
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13) {
_month = 1;
++_year;
}
}
return *this;
}
// 其他成员函数实现...
12. 实际开发中的注意事项
- 资源管理:遵循RAII原则,构造函数获取资源,析构函数释放资源
- const正确性:合理使用const修饰成员函数和参数
- 异常安全:确保操作在异常发生时不会破坏对象状态
- 移动语义:C++11后应合理实现移动构造函数和移动赋值运算符
- 三/五法则:如果需要自定义析构函数,通常也需要自定义拷贝构造和拷贝赋值
13. 常见问题与解决方案
问题1:浅拷贝导致的双重释放
现象:程序在析构时崩溃,提示双重释放
原因:使用了编译器生成的浅拷贝构造函数,多个对象共享同一资源
解决:实现自定义的深拷贝构造函数
cpp复制class String {
public:
String(const char* str = "") {
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
~String() {
delete[] _str;
}
// 深拷贝
String(const String& s) {
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}
private:
char* _str;
};
问题2:对象切片
现象:派生类对象赋值给基类对象后,派生类特有部分丢失
原因:值拷贝导致派生类部分被"切掉"
解决:使用指针或引用,或实现克隆模式
cpp复制class Base { /*...*/ };
class Derived : public Base { /*...*/ };
void func(Base b) {} // 对象切片发生处
int main() {
Derived d;
func(d); // 发生对象切片
return 0;
}
问题3:循环引用
现象:两个类互相包含对方的实例,导致编译错误
原因:类定义互相依赖
解决:使用前向声明和指针
cpp复制// 前向声明
class B;
class A {
B* b_ptr; // 使用指针而不是对象
};
class B {
A* a_ptr;
};
14. 性能优化建议
- 避免不必要的拷贝:使用引用传递大对象
- 合理使用移动语义:对于临时对象,实现移动构造函数
- 内联小函数:将简单的成员函数声明为inline
- 对象池技术:对于频繁创建销毁的对象,使用对象池
- 缓存计算结果:对于计算代价高的成员函数,缓存结果
15. 现代C++特性补充
15.1 移动语义(C++11)
移动构造函数和移动赋值运算符可以避免不必要的拷贝:
cpp复制class String {
public:
// 移动构造函数
String(String&& s) noexcept
: _str(s._str) {
s._str = nullptr;
}
// 移动赋值运算符
String& operator=(String&& s) noexcept {
if (this != &s) {
delete[] _str;
_str = s._str;
s._str = nullptr;
}
return *this;
}
};
15.2 委托构造函数(C++11)
一个构造函数可以调用同类的另一个构造函数:
cpp复制class Date {
public:
Date(int year) : Date(year, 1, 1) {} // 委托构造
Date(int year, int month, int day)
: _year(year), _month(month), _day(day) {}
};
15.3 默认和删除函数(C++11)
可以显式要求编译器生成默认版本或删除特定函数:
cpp复制class NonCopyable {
public:
NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
};
16. 设计模式中的应用
16.1 单例模式
利用静态成员实现单例:
cpp复制class Singleton {
public:
static Singleton& GetInstance() {
static Singleton instance;
return instance;
}
// 删除拷贝构造和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
};
16.2 工厂模式
利用类封装对象创建逻辑:
cpp复制class Shape {
public:
virtual void draw() = 0;
virtual ~Shape() = default;
static Shape* create(const std::string& type);
};
class Circle : public Shape { /*...*/ };
class Rectangle : public Shape { /*...*/ };
Shape* Shape::create(const std::string& type) {
if (type == "circle") return new Circle();
if (type == "rectangle") return new Rectangle();
return nullptr;
}
17. 跨平台注意事项
- 内存对齐:不同平台可能有不同的默认对齐规则
- 字节序:网络传输或文件存储时注意字节序问题
- 编译器差异:不同编译器对默认成员函数的生成策略可能不同
- 异常处理:某些嵌入式平台可能不支持异常
18. 测试与调试技巧
- 单元测试:为每个类编写单元测试
- 内存检查工具:使用valgrind等工具检测内存问题
- 打印日志:在关键函数中添加日志输出
- 防御性编程:使用assert验证前置条件
cpp复制class Complex {
public:
Complex(double real, double imag)
: _real(real), _imag(imag) {
assert(!std::isnan(_real) && !std::isnan(_imag));
}
private:
double _real;
double _imag;
};
19. 实际项目经验分享
-
类设计原则:
- 单一职责原则:一个类只做一件事
- 开闭原则:对扩展开放,对修改关闭
- 依赖倒置原则:依赖抽象而非具体实现
-
代码组织建议:
- 头文件只包含声明,实现放在源文件中
- 合理使用命名空间避免命名冲突
- 为每个类提供完整的文档注释
-
性能关键点:
- 避免在循环中创建临时对象
- 小对象可以考虑直接传值
- 高频调用的简单函数声明为inline
20. 学习资源推荐
-
书籍:
- 《Effective C++》
- 《More Effective C++》
- 《C++ Primer》
- 《深度探索C++对象模型》
-
在线资源:
- cppreference.com
- C++ Core Guidelines
- ISO C++标准文档
-
工具:
- Clang-Tidy静态分析工具
- GDB/LLDB调试器
- CMake构建系统
