类与对象
类与对象的定义
在C++中,类(Class)是一种用户定义的数据类型,用来封装数据(成员变量)和方法(成员函数),关键字为class
。可以取代C语言中的结构体,并且功能更全面。类的定义通常包括以下几个部分:
成员变量(数据成员)
:类中定义的变量,用来描述类的特征或状态。
class MyClass {
int myNumber; // 一个私有成员变量
double myDouble; // 一个公有成员变量
};
在上面的例子中,MyClass 类有两个成员变量:myNumber和 myDouble。
成员函数
:类中定义的函数,用来操作类的数据成员或提供其他功能。
class MyClass {
int myNumber;
void setNumber(int num) {
myNumber = num;
}
int getNumber() {
return myNumber;
}
};
在上面的例子中,MyClass 类有两个成员函数:setNumber 用来设置 myNumber 的值,getNumber 用来获取 myNumber 的值。
访问控制
:类中的成员可以用 private、protected 和 public 三种访问控制修饰符来限制对成员的访问。默认情况下,成员是私有的
。
class MyClass {
private:
int privateNumber;
protected:
double protectedDouble;
public:
void publicMethod() {
// 可以访问所有成员
}
};
在上面的例子中,privateNumber 是私有的,只能在类的内部访问;protectedDouble 是受保护的,可以在类的内部和派生类中访问;publicMethod 是公有的,可以被外部代码调用。
对象的创建与使用
类是一种抽象的数据类型,而对象(Object)是根据类定义的实例。创建对象的过程称为实例化。可以通过以下方式创建对象:
MyClass obj1; // 在栈上创建对象
MyClass *obj2 = new MyClass(); // 在堆上创建对象
在上面的例子中,obj1 和 obj2 都是 MyClass 类的对象。可以通过对象来访问类的成员变量和成员函数:
obj1.setNumber(42);
int number = obj1.getNumber();
这里,调用 obj1 的 setNumber 方法设置 myNumber 的值为 42,然后调用 getNumber 方法获取 myNumber 的值并赋给 number。
类域
在C++中,类(Class)的域(Scope)指的是类中成员(变量和函数)的可见性和生命周期。类域主要分为以下几种:
- 类的作用域
类中定义的成员(成员变量和成员函数)具有类作用域,即它们在整个类的范围内可见和可访问。例如:
class MyClass {
public:
int x; // 成员变量 x
void printX() {
std::cout << x << std::endl; // 可以直接访问成员变量 x
}
};
在上述示例中,x 和 printX() 都处于类 MyClass 的作用域内。
- 对象的作用域
对象(实例)的作用域是指在程序中使用对象实例化的变量或引用的范围。例如:
int main() {
MyClass obj; // 创建 MyClass 类的一个对象实例 obj
obj.x = 10; // 可以访问对象的成员变量 x
obj.printX(); // 可以调用对象的成员函数 printX()
return 0;
}
在 main() 函数中,obj 对象的作用域是从它的定义处开始直到它超出作用域(在这里是 main() 函数结束)为止。
- 成员变量的作用域
类中的成员变量具有类作用域,可以被类中的所有成员函数访问,即使它们在函数之外定义。例如:
class MyClass {
public:
int x; // 成员变量 x
void setX(int value) {
x = value; // 可以直接访问成员变量 x
}
};
在 setX() 成员函数中,可以直接访问并修改成员变量 x。
- 成员函数的作用域
类中的成员函数也具有类作用域,可以访问类中的所有成员(包括私有成员),但它们的可见性受到访问控制修饰符的限制。例如:
class MyClass {
private:
int y; // 私有成员变量 y
public:
void setY(int value) {
y = value; // 可以访问私有成员变量 y
}
};
在 setY() 成员函数中,可以直接访问并修改私有成员变量 y。
内存计算
构造函数
默认构造函数
定义与用途:
默认构造函数是在对象创建时自动调用的构造函数,如果没有显式定义构造函数,编译器会提供默认构造函数。它不带任何参数或所有参数都有默认值。
示例:
class MyClass {
public:
MyClass() {
// 可选的初始化代码
}
};
注意:
- 如果类没有其他构造函数,编译器会提供一个无参的默认构造函数。
- 默认构造函数通常用于初始化成员变量或执行其他必要的初始化操作。
参数化构造函数
定义与用途:
参数化构造函数带有参数,用来初始化对象的数据成员。可以根据需要定义多个不同的参数化构造函数。
示例:
class Point {
private:
int x, y;
public:
Point(int xCoord, int yCoord) {
x = xCoord;
y = yCoord;
}
};
注意:
- 参数化构造函数允许根据传入的参数初始化对象的各个成员变量。
- 可以定义多个参数化构造函数,根据不同的参数类型或数量进行重载。
拷贝构造函数
定义与用途:
拷贝构造函数用于通过已存在的对象创建一个新对象,与原对象相同。通常用于传递对象给函数,或者返回对象。
示例:
class MyClass {
public:
MyClass(const MyClass& other) {
// 实现拷贝构造的逻辑
}
};
注意:
- 拷贝构造函数的参数通常是常量引用,以避免不必要的拷贝。
- 如果没有显式定义,编译器会提供一个默认的拷贝构造函数。
委托构造函数
定义与用途:
委托构造函数允许一个构造函数调用同一个类的另一个构造函数来执行共同的初始化过程,减少重复代码。
示例:
委托构造函数在C++中的应用可以通过一个简单的示例来说明其用途和优势。假设我们有一个Person类,其属性包括姓名和年龄,我们可以利用委托构造函数来简化不同情况下对象的初始化过程。
#include <iostream>
#include <string>
class Person {
private:
std::string name;
int age;
public:
// 主要构造函数,委托给另一个构造函数
Person() : Person("", 0) {}
// 委托构造函数,初始化姓名和年龄
Person(const std::string& name) : Person(name, 0) {}
// 最终构造函数,完成姓名和年龄的初始化
Person(const std::string& name, int age) : name(name), age(age) {}
// 显示信息的成员函数
void display() {
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
};
int main() {
Person p1; // 使用默认构造函数,姓名为空字符串,年龄为0
p1.display();
Person p2("Alice"); // 使用带姓名参数的构造函数,年龄默认为0
p2.display();
Person p3("Bob", 25); // 使用完整的姓名和年龄构造函数
p3.display();
return 0;
}
解释说明:
主要构造函数:
Person() : Person("", 0) {}
这个构造函数使用委托构造函数的方式,将默认的姓名设置为空字符串,年龄设置为0。这样,当没有提供任何参数时,会自动调用带参数的构造函数来完成对象的初始化。
委托构造函数:
Person(const std::string& name) : Person(name, 0) {}
这个构造函数接收一个姓名参数,并将年龄设为0,然后委托给另一个构造函数来完成初始化。
最终构造函数:
Person(const std::string& name, int age) : name(name), age(age) {}
这个构造函数接收完整的姓名和年龄参数,并将成员变量name和age初始化为传入的值。
使用示例:
- Person p1;:使用默认构造函数,输出结果为 Name: , Age: 0。
- Person p2(“Alice”);:使用带姓名参数的构造函数,输出结果为 Name: Alice, Age: 0。
- Person p3(“Bob”, 25);:使用完整的姓名和年龄构造函数,输出结果为 Name: Bob, Age: 25。
通过委托构造函数,我们可以避免代码重复,保持构造函数的简洁性和清晰性。这种技术在处理复杂的对象初始化逻辑时特别有用,能够提高代码的可维护性和可读性。
注意:
- 委托构造函数使用冒号语法来调用同一类的其他构造函数。
- 构造函数之间的委托关系必须是直接的,不能形成环路。
explicit构造函数
定义与用途:
在C++中,explicit关键字用于修饰单参数的构造函数,其作用是防止编译器执行隐式类型转换,只允许显式调用该构造函数进行对象的初始化。这种机制可以避免一些意外的类型转换,提高代码的安全性和可读性。
示例说明
考虑一个简单的Date类,用来表示日期,其中包含年、月和日:
#include <iostream>
class Date {
private:
int year;
int month;
int day;
public:
// 构造函数,初始化年月日
explicit Date(int y, int m, int d) : year(y), month(m), day(d) {}
// 打印日期信息
void printDate() const {
std::cout << year << "/" << month << "/" << day << std::endl;
}
};
int main() {
Date d1 = Date(2024, 7, 14); // 直接使用构造函数初始化对象
d1.printDate();
// 下面的代码会因为 explicit 构造函数而编译错误
// Date d2 = 2024; // 错误!不能隐式调用 explicit 构造函数
// d2.printDate();
return 0;
}
示例解释:
explicit 构造函数定义:
explicit Date(int y, int m, int d) : year(y), month(m), day(d) {}
这个构造函数用于初始化日期对象,需要传入年、月、日三个参数。由于使用了 explicit 关键字,因此在使用该构造函数时,必须显式地使用构造函数语法,而不能隐式地将整数转换为 Date 对象。
显式初始化对象:
Date d1 = Date(2024, 7, 14);
这里直接使用构造函数来初始化 d1 对象,这种形式是允许的,因为使用了显式的构造函数调用。
隐式初始化的错误示例:
// 下面的代码会因为 explicit 构造函数而编译错误
Date d2 = 2024; // 错误!不能隐式调用 explicit 构造函数
如果没有 explicit 关键字,这里的代码会被编译器允许,将整数 2024 隐式转换为 Date 对象,这可能导致意外行为。因此,explicit 构造函数可以防止这种隐式转换。
适用场景:
-
避免意外类型转换: 当你希望确保对象只能通过显式构造函数调用来创建时,可以使用 explicit。这样可以防止编译器在某些情况下执行意料之外的类型转换。
-
提高代码可读性和安全性: 显示地调用构造函数可以让代码更加清晰明了,读者能够准确地了解对象的初始化过程,而不会因为隐藏的类型转换而感到困惑或出现错误。
总之,explicit 构造函数是C++中一个重要的语言特性,可以帮助程序员避免一些潜在的编程错误,提高代码的健壮性和可维护性。
删除或默认构造函数
定义与用途:
通过特殊的语法可以显式地删除或声明使用默认构造函数,这在某些情况下很有用。
在C++中,当你创建一个类时,如果没有显式定义构造函数,编译器会为你生成一个默认构造函数。默认构造函数是一个不带任何参数的构造函数,它用来初始化对象的成员变量。另外,如果你没有定义任何构造函数,编译器还会为你提供一个默认的析构函数和拷贝构造函数。
默认构造函数的生成规则
如果你显式地提供了任何构造函数(包括带默认参数的构造函数),编译器将不会自动生成默认构造函数。这意味着,如果你想让一个类的对象可以默认构造(即不传递任何参数),你需要显式地定义一个默认构造函数。
下面是一个示例:
#include <iostream>
class MyClass {
private:
int x;
double y;
public:
// 默认构造函数
MyClass() : x(0), y(0.0) {}
// 带参数的构造函数
MyClass(int x_val, double y_val) : x(x_val), y(y_val) {}
void display() {
std::cout << "x: " << x << ", y: " << y << std::endl;
}
};
int main() {
MyClass obj1; // 使用默认构造函数
obj1.display();
MyClass obj2(5, 10.5); // 使用带参数的构造函数
obj2.display();
return 0;
}
在上面的示例中:
- MyClass() 是一个默认构造函数,用来初始化 MyClass 对象的成员变量 x 和 y。
- MyClass(int x_val, double y_val) 是一个带参数的构造函数,用来按照指定的值初始化 x 和 y。
删除构造函数
在C++11及其之后的标准中,可以通过 delete 关键字删除构造函数,以禁止特定的构造方式。例如,你可以删除拷贝构造函数或移动构造函数,来禁止对象的拷贝或移动操作。下面是一个删除拷贝构造函数的示例:
class NonCopyable {
public:
// 删除拷贝构造函数
NonCopyable(const NonCopyable&) = delete;
// 默认构造函数和析构函数
NonCopyable() {}
~NonCopyable() {}
};
int main() {
NonCopyable obj1;
// NonCopyable obj2 = obj1; // 编译错误,拷贝构造函数被删除
return 0;
}
在上面的示例中,使用 NonCopyable(const NonCopyable&) = delete; 删除了拷贝构造函数,因此试图进行对象的拷贝初始化操作将导致编译错误。.
析构函数
在C++中,析构函数(Destructor)是一个特殊的成员函数,用于在对象生命周期结束时执行清理工作,例如释放动态分配的内存、关闭文件等。下面详细讨论析构函数的定义、用法、示例和一些注意事项。
析构函数的定义
析构函数的名称与类名相同,前面加上波浪号(~),不接受任何参数,也没有返回值。它在对象被销毁时自动调用。
class MyClass {
public:
// 构造函数
MyClass() {
std::cout << "Constructor called" << std::endl;
}
// 析构函数
~MyClass() {
std::cout << "Destructor called" << std::endl;
}
};
析构函数的用法
-
释放资源: 主要用于释放对象在生存期间动态分配的资源,如内存或文件句柄。
-
执行清理操作: 可以在析构函数中执行一些清理工作,确保对象被销毁时,不会留下未处理的状态。
示例与说明
考虑一个简单的类 FileHandler,用于打开和关闭文件,其中析构函数用于关闭文件句柄。
#include <iostream>
#include <fstream>
class FileHandler {
private:
std::ofstream file;
public:
// 构造函数,打开文件
FileHandler(const std::string& filename) : file(filename) {
if (!file.is_open()) {
std::cerr << "Failed to open file: " << filename << std::endl;
} else {
std::cout << "File opened: " << filename << std::endl;
}
}
// 析构函数,关闭文件
~FileHandler() {
if (file.is_open()) {
file.close();
std::cout << "File closed." << std::endl;
}
}
// 写入数据到文件
void write(const std::string& data) {
if (file.is_open()) {
file << data << std::endl;
} else {
std::cerr << "File is not open!" << std::endl;
}
}
};
int main() {
FileHandler fh("example.txt");
fh.write("Hello, World!");
// 在 main 函数结束时,fh 对象将被销毁,触发析构函数的调用,关闭文件
return 0;
}
析构函数的注意事项
执行顺序: 对象的析构函数在其生命周期结束时被自动调用,通常发生在对象超出作用域、被删除或程序结束时。
不要手动调用析构函数: 析构函数是由编译器自动调用的,不应该显式地手动调用。
虚析构函数: 如果一个类可能会作为基类使用,通常需要将析构函数声明为虚函数,以确保派生类的析构函数也能被正确调用,避免内存泄漏。
class Base {
public:
virtual ~Base() {
// 虚析构函数
}
};
class Derived : public Base {
public:
~Derived() {
// Derived 的析构函数
}
};
-
异常安全性: 析构函数应该保证在执行期间不抛出异常,因为在异常处理期间,对象的析构函数可能会被调用。
-
默认析构函数: 如果你没有显式地定义析构函数,编译器会为你生成一个默认的析构函数,该析构函数不执行任何操作。