目录
在C++中,类(Class)是面向对象编程(OOP)的核心概念之一,它允许我们将数据(成员变量)和操作数据的方法(成员函数)封装在一起,形成自定义的数据类型。类的定义和声明是掌握C++面向对象编程的基础,本文将全面解析类的定义与声明相关知识。
一、类的基本概念
1.1 什么是类
类是一种用户自定义的数据类型,它可以包含数据成员(变量)和成员函数(方法)。类就像是一个蓝图,它定义了对象的属性和行为。例如,我们可以定义一个Person类来表示一个人,这个类可能包含姓名、年龄等数据成员,以及获取姓名、增加年龄等成员函数。
1.2 类与对象的关系
对象是类的实例。类定义了对象的通用结构和行为,而对象则是根据类的定义创建的具体实体。可以将类看作是模板,而对象是根据这个模板制造出来的产品。例如,根据Person类,我们可以创建出多个具体的Person对象,每个对象都有自己独特的姓名和年龄。
1.3 类的核心组成
1.3.1 成员变量
① 存储类型对比
| 类型 | 声明方式 | 生命周期 | 内存位置 |
|---|---|---|---|
| 普通成员 | int value; | 对象生命周期 | 对象内存块 |
| 静态成员 | static int s; | 程序生命周期 | 全局数据区 |
| 常量成员 | const int c; | 对象生命周期 | 对象内存块 |
| 引用成员 | int& ref; | 对象生命周期 | 对象内存块 |
1.3.2 初始化方式
class InitDemo {
public:
// 直接初始化(C++11)
int a = 10;
// 构造函数初始化列表
InitDemo(int b_val) : b(b_val) {}
private:
const int b;
int& c = a; // 引用成员必须初始化
};
二、类的声明
2.1 声明的语法
类的声明通常使用class关键字,其基本语法如下:
class ClassName {
// 成员声明
};
这里的ClassName是类的名称,大括号内是类的成员声明部分。例如,声明一个简单的Point类:
class Point {
int x;
int y;
};
Point类包含两个数据成员x和y,用于表示二维平面上的一个点的坐标。
2.2 声明与定义的区别
类的声明只是告诉编译器有这样一个类存在,但并没有提供类的具体实现。而类的定义则需要提供类的成员函数的具体代码。例如,可以先声明一个Circle类:
class Circle {
double radius;
double getArea();
};
这里只是声明了Circle类有一个数据成员radius和一个成员函数getArea,但getArea函数的具体实现还没有给出。
三、类的定义
3.1 定义的语法
类的定义需要给出成员函数的具体实现。有两种方式可以实现类的成员函数:
① 在类内部定义
可以直接在类的大括号内给出成员函数的定义,例如:
class Rectangle {
int width;
int height;
public:
int getArea() {
return width * height;
}
};
getArea函数的定义直接放在了Rectangle类的内部。
②在类外部定义
也可以在类外部定义成员函数,但需要使用作用域解析运算符::来指定函数所属的类。例如:
class Square {
int side;
public:
int getPerimeter();
};
int Square::getPerimeter() {
return 4 * side;
}
getPerimeter函数的定义在类外部,通过Square::来表明它是Square类的成员函数。
3.2 类的成员访问控制
C++ 使用访问修饰符来控制类的成员的访问权限,主要有三种访问修饰符:public、private、protected三个关键字实现,决定了类成员的可见性和访问权限。
| 访问修饰符 | 类内部 | 类外部 | 派生类 |
|---|---|---|---|
public |
✔️ | ✔️ | ✔️ |
private |
✔️ | ❌ | ❌ |
protected |
✔️ | ❌ | ✔️ |
① public
public成员可以被类的外部代码直接访问。例如:
class Student {
public:
string name;
void displayName() {
cout << "Name: " << name << endl;
}
};
int main() {
Student s;
s.name = "Alice";
s.displayName();
return 0;
}
name和displayName都是public成员,所以可以在main函数中直接访问。
②private
private成员只能被类的成员函数访问,类的外部代码无法直接访问。例如:
class BankAccount {
private:
double balance;
public:
void deposit(double amount) {
balance += amount;
}
double getBalance() {
return balance;
}
};
int main() {
BankAccount account;
account.deposit(1000);
cout << "Balance: " << account.getBalance() << endl;
// account.balance = 2000; // 错误,不能直接访问private成员
return 0;
}
这里balance是private成员,只能通过deposit和getBalance等成员函数来访问。
③ protected
protected成员类似于private成员,但可以被派生类的成员函数访问。这在继承关系中非常有用,后续会详细介绍。
④默认访问权限
- 类中默认的访问权限是
private。 struct默认的访问权限是public。
四、构造函数和析构函数
4.1 构造函数
构造函数是一种特殊的成员函数,用于在创建对象时初始化对象的成员。构造函数的名称与类名相同,并且没有返回类型。例如:
class Book {
string title;
int pages;
public:
Book(string t, int p) {
title = t;
pages = p;
}
void displayInfo() {
cout << "Title: " << title << ", Pages: " << pages << endl;
}
};
int main() {
Book myBook("C++ Primer", 976);
myBook.displayInfo();
return 0;
}
Book类有一个构造函数,它接受两个参数title和pages,并将它们赋值给类的成员变量。
4.2 默认构造函数
如果没有定义任何构造函数,编译器会自动生成一个默认构造函数,将所有成员变量初始化为默认值(如int初始化为0,指针初始化为nullptr)。
①带参数的构造函数
可以定义带参数的构造函数来初始化对象:
class Point {
public:
double x;
double y;
// 带参数的构造函数
Point(double x_val, double y_val) {
x = x_val;
y = y_val;
}
};
int main() {
Point p(3.0, 4.0); // 使用带参数的构造函数创建对象
return 0;
}
②构造函数初始化列表
使用初始化列表可以更高效地初始化成员变量,尤其是const成员和引用成员:
class MathUtils {
public:
static int add(int a, int b) {
return a + b;
}
};
int main() {
int result = MathUtils::add(3, 5);
cout << "Result: " << result << endl;
return 0;
}
4.3 析构函数
析构函数也是一种特殊的成员函数,用于在对象销毁时执行一些清理工作,例如释放动态分配的内存。析构函数的名称是在类名前加上波浪号~,并且没有返回类型和参数。例如:
class ArrayWrapper {
int* arr;
int size;
public:
ArrayWrapper(int s) {
size = s;
arr = new int[size];
}
~ArrayWrapper() {
delete[] arr;
}
};
int main() {
{
ArrayWrapper wrapper(10);
// 对象在离开这个代码块时会自动调用析构函数
}
return 0;
}
ArrayWrapper类的析构函数负责释放动态分配的数组内存。
五、类的友元
5.1 友元函数
友元函数是一种可以访问类的私有和保护成员的非成员函数。要将一个函数声明为类的友元函数,需要在类的定义中使用friend关键字。例如:
class Rectangle {
int width;
int height;
public:
Rectangle(int w, int h) : width(w), height(h) {}
friend int getArea(const Rectangle& rect);
};
int getArea(const Rectangle& rect) {
return rect.width * rect.height;
}
int main() {
Rectangle rect(5, 3);
cout << "Area: " << getArea(rect) << endl;
return 0;
}
getArea函数是Rectangle类的友元函数,所以它可以直接访问Rectangle类的私有成员width和height。
5.2 友元类
除了友元函数,还可以将一个类声明为另一个类的友元类。友元类的所有成员函数都可以访问被友元类的私有和保护成员。例如:
class ClassA {
private:
int data;
public:
ClassA(int d) : data(d) {}
friend class ClassB;
};
class ClassB {
public:
void displayData(const ClassA& obj) {
cout << "Data in ClassA: " << obj.data << endl;
}
};
int main() {
ClassA a(10);
ClassB b;
b.displayData(a);
return 0;
}
这里ClassB是ClassA的友元类,所以ClassB的成员函数displayData可以访问ClassA的私有成员data。
六、类的静态成员
6.1 静态数据成员
静态数据成员是类的所有对象共享的成员,它不属于任何一个具体的对象。静态数据成员需要在类内部声明,在类外部定义和初始化。例如:
class Counter {
public:
static int count;
Counter() {
count++;
}
};
int Counter::count = 0;
int main() {
Counter c1;
Counter c2;
cout << "Number of objects: " << Counter::count << endl;
return 0;
}
count是Counter类的静态数据成员,每次创建Counter对象时,count的值就会加 1。
6.2 静态成员函数
静态成员函数也是类的所有对象共享的函数,它只能访问静态数据成员,不能访问非静态成员。静态成员函数可以通过类名直接调用,不需要创建对象。例如:
class MathUtils {
public:
static int add(int a, int b) {
return a + b;
}
};
int main() {
int result = MathUtils::add(3, 5);
cout << "Result: " << result << endl;
return 0;
}
这里add是MathUtils类的静态成员函数,可以直接通过类名调用。
七、类的继承与派生
继承是面向对象编程的重要特性,允许派生类继承基类的成员。
7.1 继承的基本语法
class DerivedClass : access_specifier BaseClass {
// 派生类新增成员
};
access_specifier:继承方式,包括public(公有继承)、protected(保护继承)、private(私有继承)。
7.2 示例:公有继承
class Shape {
public:
void setColor(const std::string& color) {
this->color = color;
}
std::string getColor() const {
return color;
}
private:
std::string color;
};
class Circle : public Shape {
public:
void setRadius(double radius) {
this->radius = radius;
}
double getRadius() const {
return radius;
}
private:
double radius;
};
int main() {
Circle c;
c.setColor("Red"); // 允许:公有继承可以访问基类的公有成员
std::cout << "Color: " << c.getColor() << std::endl; // 输出:Color: Red
return 0;
}
7.3 继承方式的影响
| 继承方式 | 基类public成员 |
基类protected成员 |
基类private成员 |
|---|---|---|---|
public继承 |
public |
protected |
不可访问 |
protected继承 |
protected |
protected |
不可访问 |
private继承 |
private |
private |
不可访问 |
八、类的多态
多态是面向对象编程的另一个重要特性,允许不同类的对象对同一消息作出不同的响应。
8.1 虚函数
在基类中使用virtual关键字声明虚函数,派生类可以重写该函数。
class Shape {
public:
virtual void draw() const {
std::cout << "Drawing a shape" << std::endl;
}
};
class Circle : public Shape {
public:
void draw() const override {
std::cout << "Drawing a circle" << std::endl;
}
};
int main() {
Shape* shape = new Circle();
shape->draw(); // 输出:Drawing a circle
delete shape;
return 0;
}
8.2 纯虚函数与抽象类
在基类中定义纯虚函数(= 0),使基类成为抽象类,不能实例化。
class Shape {
public:
virtual void draw() const = 0; // 纯虚函数
};
class Circle : public Shape {
public:
void draw() const override {
std::cout << "Drawing a circle" << std::endl;
}
};
int main() {
Shape* shape = new Circle();
shape->draw(); // 输出:Drawing a circle
delete shape;
return 0;
}
九、类设计最佳实践
9.1 SOLID原则
| 原则 | 说明 | 示例 |
|---|---|---|
| 单一职责 | 一个类只做一件事 | 分离数据存储和格式转换 |
| 开闭原则 | 对扩展开放,对修改关闭 | 使用策略模式代替修改类 |
| 里氏替换 | 子类必须能替换基类 | 保持继承关系的语义一致性 |
| 接口隔离 | 多个专用接口优于单个通用接口 | 分解庞大的接口类 |
| 依赖倒置 | 依赖抽象而非具体实现 | 使用抽象基类定义接口 |
9.2 异常安全保证
| 安全等级 | 说明 |
|---|---|
| 基本保证 | 对象保持有效状态,无资源泄漏 |
| 强保证 | 操作要么完全成功,要么保持原状态 |
| 不抛保证 | 承诺不抛出任何异常 |
十、常见问题与调试技巧
10.1 典型编译错误
-
未定义引用:忘记实现类成员函数
-
访问权限错误:误用private成员
-
虚函数不匹配:签名不一致导致隐藏而非重写
-
对象切片:值传递多态对象
10.2 调试技巧
使用gdb查看对象内存布局:
(gdb) p /x *(ClassType*)obj_ptr
打印虚函数表:
(gdb) info vtbl obj_ptr
Valgrind检测内存泄漏:
valgrind --leak-check=full ./program
十一、总结
类是 C++ 面向对象编程的核心,掌握类的定义和声明对于编写高质量的 C++ 代码至关重要。通过不断实践和学习,能够更深入地理解和运用类的各种特性,开发出更加复杂和强大的程序。
十二、参考资料
- 《C++ Primer(第 5 版)》这本书是 C++ 领域的经典之作,对 C++ 的基础语法和高级特性都有深入讲解。
- 《Effective C++(第 3 版)》书中包含了很多 C++ 编程的实用建议和最佳实践。
- 《C++ Templates: The Complete Guide(第 2 版)》该书聚焦于 C++ 模板编程,而
using声明在模板编程中有着重要应用,如定义模板类型别名等。 - C++ 官方标准文档:C++ 标准文档是最权威的参考资料,可以查阅最新的 C++ 标准(如 C++11、C++14、C++17、C++20 等)文档。例如,ISO/IEC 14882:2020 是 C++20 标准的文档,可从相关渠道获取其详细内容。