类的定义
C++中定义类通常包括以下几个部分:
- 类关键字:使用
class
关键字来定义一个类。 - 类名:遵循命名规则,首字母大写。
- 类的成员:包括数据和函数成员。
- 访问修饰符:public、private 和 protected,用于定义类成员的访问级别。
以下是一个简单的C++类的定义示例:
#include <iostream>
using namespace std;
class MyClass {
public:
// 构造函数
MyClass(int val) : privateValue(val) {}//初始化列表, 下面会讲
// 析构函数
~MyClass() {}
// 成员函数
void setValue(int val) {
privateValue = val;
}
int getValue() {
return privateValue;
}
private:
// 私有成员
int privateValue;
};
这个例子中,MyClass
是一个包含一个私有成员变量 privateValue
和两个公共成员函数 setValue
和 getValue
的类。构造函数用于初始化对象,析构函数用于在删除对象时执行清理操作。
对象的创建
一旦定义了类,就可以创建该类的对象。创建对象的过程包括声明一个类的变量,并可以使用构造函数来初始化它。
以下是如何创建 MyClass
类的对象:
int main() {
// 创建对象
MyClass obj(10); // 使用构造函数初始化
// 访问成员函数
obj.setValue(20);
cout << "Value: " << obj.getValue() << endl;
return 0;
}
在上面的代码中,obj
是一个 MyClass
类型的对象,通过传递参数 10
给构造函数来创建。之后,使用成员函数 setValue
设置 privateValue
的值,然后使用 getValue
函数来获取它的值。
构造函数和析构函数是C++中类的两个特殊成员函数,它们在对象的生命周期中扮演着重要的角色。
构造函数
构造函数是一种特殊的成员函数,当一个对象被创建时,构造函数会被自动调用,用于初始化对象的数据成员。以下是构造函数的一些特点:
- 命名规则:构造函数的名称与类名完全相同。
- 无返回类型:构造函数没有返回类型,即使是void类型也不行。
- 调用时机:对象被创建时,或者通过new关键字动态分配内存时,构造函数会被调用。
- 重载:构造函数可以重载,即可以有多个构造函数,只要它们的参数列表不同即可。
示例:
class MyClass {
public:
MyClass() { // 默认构造函数
// 初始化代码...
}
MyClass(int value) { // 带参数的构造函数
// 初始化代码,可以使用参数value...
}
};
在上面的例子中,MyClass
类有两个构造函数:一个是默认构造函数,另一个是带有一个整型参数的构造函数。
析构函数
析构函数是另一种特殊的成员函数,当一个对象的生命周期结束时,例如离开其作用域或者被delete删除时,析构函数会被调用,用于执行对象清理工作,如释放分配的资源。
以下是析构函数的一些特点:
- 命名规则:析构函数的名称是类名前面加上一个波浪号(
~
)。 - 无参数:析构函数不接受任何参数。
- 无返回类型:析构函数没有返回类型。
- 只有一个析构函数:一个类只能有一个析构函数,如果未自定义,编译器会生成一个默认的析构函数。
示例:
class MyClass {
public:
MyClass() {
// 初始化代码...
}
~MyClass() { // 析构函数
// 清理代码...
}
};
在上面的例子中,MyClass
类有一个析构函数,它将在对象销毁时被调用。
为什么要使用构造函数和析构函数?
- 初始化和清理:构造函数确保对象在创建时被正确初始化,析构函数确保在对象销毁时能够执行必要的清理操作。
- 资源管理:对于管理动态分配的内存、打开的文件柄、网络连接等资源,构造函数和析构函数是非常重要的。
- 确定性:通过在构造函数和析构函数中定义行为,程序员可以确保对象的创建和销毁是确定性的,避免资源泄漏和其他问题。
初始化列表
初始化列表是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
class MyClass
{
public:
// 构造函数
MyClass(int val) : privateValue1(val),privateValue2(val) {}
private:
int privateValue1;
int privateValue2;
};
说明:类成员是按照它们在类里被声明的顺序进行初始化的,与它们在成员初始化列表中列出的顺序无关。
访问修饰符
-
public:公共成员可以在任何地方被访问。这意味着任何外部代码都可以调用类的公共成员函数和访问公共数据成员。
-
private:私有成员只能在类的内部被访问。这意味着私有成员对外部代码不可见,也不能被外部代码直接访问,从而确保了类的封装性。
-
protected:保护成员可以在类的内部以及派生类中被访问,但是不能被外部代码访问。这提供了对派生类的一种特殊访问权限,而不会暴露给其他外部代码。
这些访问修饰符的使用,确保了类的实现细节(通常被声明为私有)对外部代码不可见,同时允许外部代码通过公共接口与类进行交互。这是一种实现封装和抽象的方法,使得类可以隐藏其复杂性,仅暴露必要的功能。以下是一个简单的示例:
class Box {
private:
int width; // 私有成员,外部无法直接访问
public:
int height; // 公共成员,外部可以直接访问
// 构造函数
Box(int w, int h) : width(w), height(h) {}
// 公共成员函数,提供对私有成员的访问和修改
int getWidth() const
{
return width;
}
void setWidth(int w)
{
width = w;
}
// 其他成员函数...
};
在这个例子中,width
是一个私有成员,不能在类的外部直接访问或修改。为了访问和修改 width
,提供了公共成员函数 getWidth
和 setWidth
。另一方面,height
是一个公共成员,可以直接从类的外部访问。这就是访问控制在C++中的基本用法。
拷贝构造函数
拷贝构造函数的声明通常如下:
ClassName(const ClassName &source);
这个函数接收一个对同类对象的常量引用作为参数。当以下情况发生时,拷贝构造函数会被调用:
- 通过已存在的对象初始化一个新对象。
- 按值传递对象到函数内部,即对象作为函数的参数通过值传递方式传入函数体内部时。
- 在返回对象时,如果采用值返回方式。
浅拷贝和深拷贝
浅拷贝只是简单地复制对象的所有成员变量值,对于指针类型的成员变量来说,浅拷贝会复制指针的值,这意味着复制后的对象和原对象将指向相同的内存地址。如果类中有指针指向动态分配的内存,浅拷贝可能会导致两个问题:
- 内存泄漏:如果其中一个对象被销毁,它的析构函数可能会尝试释放这块内存,随后另一个对象也尝试释放同一块内存,这将导致未定义的行为或程序崩溃。
- 悬挂指针:如果原对象修改了指针指向的内容,复制对象也将反映出这些更改,因为它们共享相同的数据。
深拷贝在复制对象时,会为指针成员分配新的内存,并复制指针所指向的数据,确保每个对象都有其数据独立的副本。这样,修改一个对象的指针成员所指向的数据不会影响另一个对象。
运算符重载的基本规则
- 不能创建新的运算符:只能重载已存在的运算符。
- 不能改变运算符的优先级:重载后的运算符保持原有的优先级。
- 不能改变运算符的操作数数量:重载的运算符应保持原有的参数数量和类型。
- 有些运算符不能被重载:如
.*
::
sizeof
?:
.
。 - 重载后的运算符不能是静态的:它们通常与类的特定实例相关联。
运算符重载的两种形式
运算符重载可以是成员函数或非成员函数:
-
成员函数:当运算符是二元运算符,且左侧的操作数是类的对象时,可以将该运算符重载为类的成员函数。此时,左侧的操作数隐式地作为
this
指针传递,右侧的操作数作为参数传递。 -
非成员函数:当运算符是二元运算符,且左侧的操作数不是类的对象,或者重载的是一元运算符时,可以将运算符重载为类的非成员函数。此时,两个操作数都需要作为参数传递。
示例
让我们创建一个表示二维坐标点的类 Point
,然后重载加法运算符 +
以便我们可以将两个点相加。
class Point {
private:
int x, y; // 表示点的坐标
public:
// 构造函数
Point(int xVal = 0, int yVal = 0) : x(xVal), y(yVal) {}
// 重载加法运算符
Point operator+(const Point& other) const
{
// 创建一个新的Point对象,其坐标是两个点坐标的和
return Point(x + other.x, y + other.y);
}
// 用于显示点的坐标
void display() const
{
std::cout << "(" << x << ", " << y << ")" << std::endl;
}
};
在这个例子中,Point
类有两个私有成员变量 x
和 y
,它们分别代表二维空间中的一个点的水平和垂直坐标。
使用重载的运算符
现在我们可以创建 Point
对象,并使用重载的加法运算符将它们相加:
int main() {
Point p1(1, 2); // 创建一个点 (1, 2)
Point p2(3, 4); // 创建另一个点 (3, 4)
Point p3 = p1 + p2; // 使用重载的加法运算符将 p1 和 p2 相加
p3.display(); // 显示结果 (4, 6)
return 0;
}
在这个 main
函数中,我们创建了两个 Point
对象 p1
和 p2
,然后使用 +
运算符将它们相加。重载的 +
运算符函数会被调用,返回一个新的 Point
对象 p3
,其坐标是 p1
和 p2
坐标的和。最后,我们调用 display
方法来输出 p3
的坐标。
this
指针
this
指针是C++中的一个特殊指针,它指向当前类实例(即当前对象)的地址。在每个成员函数中,this
指针都是可用的,它允许成员函数访问和修改当前对象的成员变量。
以下是与 this
指针相关的几个要点:
this
指针的特点
- 指向当前对象:
this
指针指向当前正在执行成员函数的对象的内存地址。 - 隐式传递:当调用成员函数时,
this
指针被隐式地传递给成员函数,无需在函数参数中声明。 - 类型为指向当前类类型的指针:
this
指针的类型是const
类型,这意味着它不能被用于修改它所指向的对象的地址,但可以用来访问和修改对象的成员。
使用 this
指针的情景
- 区分局部变量和成员变量:当成员函数的参数名称与类的成员变量名称相同时,可以使用
this
指针来区分它们。 - 返回当前对象:如果成员函数需要返回当前对象的引用,可以直接使用
this
指针。
示例
class Example {
private:
int value;
public:
Example(int val) : value(val) {
// 使用 this 指针设置成员变量
this->value = val;
}
int getValue() const {
return this->value; // 使用 this 指针访问成员变量
}
void setValue(int val) {
this->value = val; // 使用 this 指针修改成员变量
}
Example& getThis() {
return *this; // 返回当前对象的引用
}
};
在上面的 Example
类中,我们在构造函数中使用 this->value = val;
来设置成员变量 value
的值,以区分构造函数的参数 val
和成员变量 value
。在 getValue
和 setValue
成员函数中,我们也使用 this->value
来访问和修改成员变量。getThis
函数返回当前对象的引用,直接通过 return *this;
实现。
注意
this
指针是成员函数的一个隐含参数,因此在成员函数内部,你可以不使用this
指针直接访问成员变量,编译器会自动帮你生成对应的代码。但是,在某些情况下(如需要返回当前对象的引用或指针),使用this
指针可以使代码更清晰、更易于理解。
static成员
概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
特性
- 1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
- 2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
- 3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
- 4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
- 5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制
友元函数和友元类
在C++中,友元是一种特殊的机制,它允许一个类的非成员函数或者另一个类访问该类的私有(private)和受保护(protected)成员。友元关系提供了一种打破类封装性的方式,这在某些情况下可以提供编程上的便利,但同时也需要注意其对封装性的潜在破坏。
以下是关于C++中友元的几个关键点:
友元函数
-
定义:友元函数是一个非成员函数,但它可以访问类中的私有和受保护成员。要在类中声明一个友元函数,需要在类定义中使用关键字
friend
,并指定函数的声明。示例代码:
class Box { private: int width; public: Box(int w) : width(w) {} friend void printWidth(const Box& b); // 友元函数声明 }; void printWidth(const Box& b) { cout << "Width of box: " << b.width; // 访问私有成员 }
-
使用场景:友元函数通常用于执行某些需要直接访问类私有数据的操作,而这些操作又不适合作为类的成员函数。
-
优点:可以提高程序的运行效率,因为它避免了类的成员函数调用的额外开销,比如类型检查和安全性检查。
-
缺点:破坏了类的封装性,因为友元函数可以直接访问类的内部工作状态。
友元类
-
定义:除了友元函数,C++还允许整个类成为另一个类的友元。这意味着友元类的所有成员函数都可以访问另一个类的私有和受保护成员。
示例代码:
class Box; class Printer { public: void print(const Box& b); }; class Box { private: int width; public: Box(int w) : width(w) {} friend class Printer; // 友元类声明 }; void Printer::print(const Box& b) { cout << "Width of box: " << b.width; // 访问私有成员 }
-
使用场景:友元类通常用于两个类之间存在密切合作关系,且一个类的成员函数需要访问另一个类的私有成员。
注意事项
- 单向性:友元关系是单向的,如果类A是类B的友元,并不意味着类B也是类A的友元。
- 非传递性:友元关系不是传递的,如果类A是类B的友元,类B是类C的友元,并不意味着类A是类C的友元。
- 谨慎使用:虽然友元可以提供编程上的便利,但由于它破坏了封装性,因此在使用时需要谨慎权衡其优缺点。