目录
在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 标准的文档,可从相关渠道获取其详细内容。