C++ 作为一种强大的面向对象编程语言,为开发者提供了丰富的工具来实现代码的复用、封装和抽象。本文将详细介绍 C++ 中的类与对象,探讨如何利用这些特性来设计和实现复杂的软件系统。
目录
什么是类和对象?
在 C++ 中,类是一种自定义的数据类型,它是对现实世界中事物的抽象。类定义了对象的属性(数据成员)和行为(成员函数)。对象则是类的实例,表示实际的实体。对象根据类的定义来拥有特定的属性和行为。
类与对象的关系
- 类是模板:类定义了一组属性和行为,提供了创建对象的模板。
- 对象是实例:对象是根据类的模板创建的具体实例,具有类所定义的属性和行为。
定义和声明类
在 C++ 中,类的定义包含在花括号 {}
中,并以分号 ;
结束。一个基本的类定义如下:
class ClassName {
public:
// 公有成员变量和成员函数
private:
// 私有成员变量和成员函数
protected:
// 受保护成员变量和成员函数
};
类的成员
- 数据成员(属性):表示对象的状态。
- 成员函数(方法):表示对象的行为。
- 访问控制修饰符:控制对类成员的访问权限,包括
public
、private
和protected
。
示例类
下面是一个简单的 Car
类的示例:
#include <iostream>
#include <string>
class Car {
public:
// 构造函数
Car(const std::string& model, int year) : model_(model), year_(year) {}
// 成员函数
void displayInfo() const {
std::cout << "Model: " << model_ << ", Year: " << year_ << std::endl;
}
void setModel(const std::string& model) {
model_ = model;
}
void setYear(int year) {
year_ = year;
}
std::string getModel() const {
return model_;
}
int getYear() const {
return year_;
}
private:
// 数据成员
std::string model_;
int year_;
};
创建和使用对象
对象的创建和使用通常发生在 main
函数或其他函数中。以下示例展示了如何创建对象并调用其成员函数:
int main() {
Car myCar("Toyota", 2022); // 创建一个 Car 对象
myCar.displayInfo(); // 调用对象的成员函数
myCar.setModel("Honda");
myCar.setYear(2023);
std::cout << "Updated Car Info: " << myCar.getModel() << ", " << myCar.getYear() << std::endl;
return 0;
}
类的默认成员函数
构造函数
- 函数名与类名相同
- 对于内置类型(基本类型)成员变量 , 编译器默认生成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。
- 对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要用 初始化列表才能解决
- 无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数,都叫做默认构造函数, 总结一下就是不传实参就可以调用的构造就叫默认构造。
析构函数
- 析构函数名是在类名前加上字符 ~。
- 跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,自定类型成员会调用他的析构函数。
- 还需要注意的是我们显示写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数。
- 如果类中没有申请资源(malloc之类的) 时,析构函数可以不写,直接使用编译器生成的默认析构函数。
- 一个局部域的多个对象,C++规定后定义的先析构。
拷贝构造函数
- 拷贝构造函数是构造函数的一个重载。
- 拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。 拷贝构造函数也可以多个参数,但是第一个参数必须是类类型对象的引用,后面的参数必须有缺省值。
- C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
- 编译器自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的赋值重载函数。所以当类中申请资源时, 需要显式实现。
赋值运算符重载
- 赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成const 当前类类型引用,否则会传值传参会有拷贝
- 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。
- 没有显式实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的赋值重载函数。
取地址运算符重载
取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,这两个函数编译器自动生成的就可以够我们用了,不需要去显示实现。
访问控制
C++ 提供了三种访问控制修饰符:
- public:公共成员可以被类的外部访问。
- private:私有成员只能被类的内部访问,外部无法访问。
- protected:受保护成员可以被类的派生类访问,但不能被外部直接访问。
static成员
静态变量
- 类共享:静态变量被类的所有对象共享,而不是每个对象单独拥有一个拷贝。这意味着在类的所有实例之间,静态变量只有一份。
- 生命周期:静态变量在程序启动时分配内存,并在程序结束时释放。这与普通成员变量不同,普通成员变量的生命周期与其所属对象的生命周期一致。
- 访问权限:静态变量可以通过对象实例或直接通过类名来访问,通常推荐使用类名来访问以强调其与类相关的特性。
- 初始化:静态变量必须在类的外部进行初始化,除非它是一个常量表达式并在类定义中被声明为
constexpr
。
-
示例
#include <iostream>
class MyClass {
public:
static int count; // 静态变量声明
MyClass() {
count++; // 构造函数中递增静态变量
}
~MyClass() {
count--; // 析构函数中递减静态变量
}
};
// 静态变量初始化
int MyClass::count = 0;
int main() {
MyClass obj1;
MyClass obj2;
std::cout << "Count: " << MyClass::count << std::endl; // 输出: Count: 2
{
MyClass obj3;
std::cout << "Count: " << MyClass::count << std::endl; // 输出: Count: 3
} // obj3超出作用域被销毁
std::cout << "Count: " << MyClass::count << std::endl; // 输出: Count: 2
return 0;
}
静态函数
- 不能访问非静态成员:静态函数无法访问类的非静态成员(变量或函数),因为它们没有一个具体的对象实例与之关联。
- 独立性:由于静态函数不依赖于对象实例,它们可以在不创建对象的情况下被调用。
- 使用场景:静态函数通常用于实现与整个类相关的功能,而不需要访问任何特定对象的状态。
-
示例
#include <iostream>
class MyClass {
public:
static int count;
MyClass() {
count++;
}
~MyClass() {
count--;
}
static int getCount() {
return count;
}
};
int MyClass::count = 0;
int main() {
MyClass obj1;
MyClass obj2;
std::cout << "Count: " << MyClass::getCount() << std::endl; // 输出: Count: 2
MyClass* obj3 = new MyClass();
std::cout << "Count: " << MyClass::getCount() << std::endl; // 输出: Count: 3
delete obj3;
std::cout << "Count: " << MyClass::getCount() << std::endl; // 输出: Count: 2
return 0;
}
总结
- 共享性:静态成员在类的所有对象中共享。
- 生命周期:静态成员的生命周期贯穿程序的整个运行过程。
- 访问性:静态成员可以通过类名直接访问。
- 使用场景:当需要在多个对象之间共享数据或提供与特定对象无关的功能时,静态成员是非常有用的工具。
友元
友元(Friend)是一种特殊的关系,允许一个类以外的函数或另一个类访问其私有(private)和受保护(protected)的成员。通过使用友元,类可以对外提供对其内部状态的访问权限,而不需要将这些成员公开为公共(public)。这在保持类的封装性和数据安全的同时,允许对某些外部函数或类提供更高的访问权限。
友元函数
友元函数是一种普通的函数,但通过使用 friend
关键字,可以让它访问一个类的私有和受保护成员。它通常用于重载运算符或者实现某些需要直接访问类的私有成员的特殊功能。
语法
class MyClass {
private:
int data;
public:
// 声明 friend 函数
friend void myFriendFunction(MyClass& obj);
};
// 定义 friend 函数
void myFriendFunction(MyClass& obj) {
// 可以直接访问 MyClass 的私有成员
obj.data = 10;
}
友元类
友元类是另一个类,使用 friend
关键字声明,它可以访问本类的所有私有和受保护成员。这种设计常用于两个类需要高度协作的场景。
语法
class MyClass {
private:
int data;
// 声明 friend 类
friend class FriendClass;
};
class FriendClass {
public:
void accessMyClass(MyClass& obj) {
// 可以直接访问 MyClass 的私有成员
obj.data = 20;
}
};
友元的使用场景
-
运算符重载:友元函数可以用来重载运算符,尤其是那些需要访问类的私有成员的运算符,比如二元运算符(+,-,==,!= 等)。
-
类之间的密切合作:当两个类需要频繁互相访问对方的私有成员时,将其中一个类声明为另一个类的友元,可以简化代码设计。
-
访问控制:通过友元函数或类,可以实现对外部代码的有限度访问,而不必将所有成员都公开为公共成员。这种方式可以保持类的封装性。
注意事项
-
破坏封装性:使用友元会在一定程度上破坏封装性,因为友元可以访问类的私有和受保护成员。因此,应谨慎使用友元,尽量避免过度依赖。
-
双向友元关系:友元关系是单向的。如果
ClassA
是ClassB
的友元,那么ClassB
并不自动成为ClassA
的友元。 -
友元与继承无关:友元关系与继承无关,即友元关系不会传递给派生类。只有显式声明的类或函数才是友元。
-
访问权限:友元函数和友元类依然需要通过合法对象来访问其成员,无法直接通过类名访问非静态成员。
初始化列表
在 C++ 中,初始化列表是类的构造函数的一部分,用于在对象创建时直接初始化成员变量。它提供了一种在构造函数体执行之前对类成员进行初始化的方式,尤其是在初始化 const 成员、引用类型成员和调用基类的构造函数时非常有用。
初始化列表的语法
初始化列表紧跟在构造函数的参数列表之后,以冒号(:)开头,后面是成员变量的初始化表达式。每个初始化表达式用逗号分隔。
初始化列表的优点
-
效率:使用初始化列表可以避免不必要的默认构造和赋值操作。对于非基本类型的数据成员,直接初始化可以减少对象构造的开销,提高程序效率。
-
初始化 const 成员:
const
成员必须在初始化列表中进行初始化,因为它们的值在对象的生命周期内是不可变的,无法在构造函数体内被赋值。 -
初始化引用类型成员:引用类型成员必须在初始化列表中进行初始化,因为引用一旦被绑定到某个对象,就不能再被重新绑定。
-
调用基类构造函数:在派生类的初始化列表中,可以指定调用基类的哪个构造函数,以便正确地初始化基类的部分。
-
成员的初始化顺序:初始化列表按照成员声明的顺序执行,而不是按照列表中指定的顺序。这意味着初始化列表中的顺序应该和成员变量的声明顺序一致,以避免潜在的依赖问题。
示例
以下是一个完整的示例,展示了如何使用初始化列表来初始化各种类型的成员:
#include <iostream>
#include <string>
class Base {
public:
Base(int value) {
std::cout << "Base class constructor called with value: " << value << std::endl;
}
};
class MyClass : public Base {
private:
int a;
double b;
const int c;
int& ref;
std::string str;
public:
// 构造函数,使用初始化列表初始化成员和基类
MyClass(int x, double y, int z, int& r, const std::string& s)
: Base(x), a(x), b(y), c(z), ref(r), str(s) {
std::cout << "MyClass constructor called." << std::endl;
}
void display() {
std::cout << "a: " << a << ", b: " << b << ", c: " << c
<< ", ref: " << ref << ", str: " << str << std::endl;
}
};
int main() {
int refValue = 42;
MyClass obj(10, 20.5, 30, refValue, "Hello");
obj.display();
return 0;
}
注意事项
-
初始化顺序:初始化列表的执行顺序严格按照成员变量在类中声明的顺序,而不是初始化列表中的顺序。如果两个成员变量有依赖关系,确保它们的声明顺序是正确的。
-
基类构造函数的调用:如果基类没有默认构造函数,派生类必须在初始化列表中显式调用基类的构造函数。
-
异常安全性:在异常处理环境中,初始化列表可以确保在对象构造期间,如果发生异常,只有部分对象被构造,从而可以避免资源泄露。