封装时的访问权限
访问权限表格
访问权限 | 本类内部访问 | 子类访问 | 类外部访问 |
---|---|---|---|
public (公有) | 可以 | 可以 | 可以 |
protected (保护) | 可以 | 可以 | 不可以 |
private (私有) | 可以 | 不可以 | 不可以 |
默认权限(private ) | 可以 | 不可以 | 不可以 |
设置访问权限的策略
情况 | 访问权限 | 说明 |
---|---|---|
只在本类中使用 | private | 当成员只在类内部使用,不需要对子类或类外暴露时,使用private 。 |
在本类和子类中使用 | protected | 当成员需要被子类访问,但不希望被类外访问时,使用protected 。 |
需要类外部访问 | public | 当成员需要被类外部代码访问时,使用public 。 |
解释与应用
-
private
:- 只能在类的内部访问,子类和类外部代码无法访问。适用于封装类的实现细节,不希望其他代码(包括子类)直接访问。
class MyClass { private: int privateVar; // 仅在本类内部访问 };
-
protected
:- 允许子类访问,但类外部代码无法访问。适用于希望子类继承和扩展,但仍然保持对类外的封装。
class BaseClass { protected: int protectedVar; // 子类可以访问 };
-
public
:- 对所有代码开放,无论是本类、子类还是类外部。适用于需要作为接口给外部使用的成员。
class MyClass { public: int publicVar; // 所有代码都可以访问 };
实际应用场景的策略
private
: 成员变量和辅助函数,只有本类需要使用,不希望暴露给子类或外部使用。protected
: 成员函数或变量需要被子类使用或重写,但不希望外部代码直接访问。例如,模板方法设计模式中的钩子方法通常是protected
。public
: 类对外的接口,所有需要外部代码调用的成员函数或变量。
构造函数
1. 构造函数
**构造函数是与类名相同的特殊成员函数,用于在创建对象时初始化对象的状态。**它没有返回值类型,也不能显式返回任何值。构造函数可以被重载,这意味着同一个类可以有多个构造函数,只要它们的参数列表不同。
注意事项:
- 构造函数不能有返回类型,包括
void
。 - 构造函数可以被重载,这是通过参数列表的不同来实现的。
示例:
class MyClass {
public:
MyClass() {
// 默认构造函数
std::cout << "Default Constructor Called" << std::endl;
}
MyClass(int x) {
// 带参数的构造函数
std::cout << "Parameterized Constructor Called with value " << x << std::endl;
}
};
int main() {
MyClass obj1; // 调用默认构造函数
MyClass obj2(10); // 调用带参数的构造函数
return 0;
}
扩展: 在上面的代码中,MyClass
有两个构造函数,一个是无参构造函数,另一个是带一个整数参数的构造函数。这两个构造函数展示了构造函数的重载特性。
2. 构造函数的作用
**构造函数的主要作用是在对象创建时对其数据成员进行初始化。**它会在对象分配内存后立即被调用。构造函数除了初始化数据成员外,还可以调用其他初始化函数或执行其他操作。
注意事项:
- 如果数据成员是对象类型,构造函数还会调用这些对象的构造函数。
- 构造函数可以用于分配资源(如动态内存、文件句柄等),但如果在构造函数中分配资源,通常需要在析构函数中释放这些资源,以避免内存泄漏。
示例:
class MyClass {
private:
int value;
public:
MyClass(int v) : value(v) {
std::cout << "Constructor initializing value to " << value << std::endl;
}
};
int main() {
MyClass obj(100); // 构造函数初始化成员变量 value 为 100
return 0;
}
扩展: 在这个例子中,MyClass
的构造函数使用了初始化列表的方式来初始化value
。这种方式比在构造函数体内赋值更高效,因为它直接初始化数据成员,而不是先默认构造再赋值。
3. 构造函数的执行
构造函数是在对象创建时自动调用的,这意味着无论是栈上创建对象,还是在堆上使用new
操作符创建对象,构造函数都会被执行。
注意事项:
- 栈上创建对象时,构造函数会在对象声明的同时被调用。
- 堆上创建对象时,
new
或new[]
操作符会自动调用构造函数。 - 对于数组对象,每个元素的构造函数都会被调用。
示例:
class MyClass {
public:
MyClass() {
std::cout << "Constructor Called" << std::endl;
}
};
int main() {
MyClass obj; // 栈上创建对象,自动调用构造函数
MyClass* pObj = new MyClass(); // 堆上创建对象,自动调用构造函数
delete pObj; // 不要忘记释放堆内存
return 0;
}
扩展: 上面的例子展示了如何在栈上和堆上创建对象。需要注意的是,对于堆上创建的对象,必须使用delete
释放内存,否则会导致内存泄漏。
4. 没有调用构造函数的情况
有些情况下,对象创建时并不会调用构造函数,这些情况通常与对象的复制或传递方式有关。
注意事项:
- 对象复制:当一个已初始化的对象赋值给另一个未初始化的对象时,不会调用构造函数,而是调用赋值运算符。
- 对象传递:当对象以值的方式作为函数参数传递时,会调用拷贝构造函数,而不是通常的构造函数。
示例:
class MyClass {
public:
MyClass() {
std::cout << "Default Constructor Called" << std::endl;
}
MyClass(const MyClass& other) {
std::cout << "Copy Constructor Called" << std::endl;
}
};
void func(MyClass obj) {
// 使用拷贝构造函数创建形参对象
}
int main() {
MyClass obj1; // 调用默认构造函数
MyClass obj2 = obj1; // 调用拷贝构造函数
func(obj1); // 传递对象时调用拷贝构造函数
return 0;
}
扩展: 当对象obj1
赋值给obj2
时,调用的是拷贝构造函数,而不是通常的构造函数。同样,在将对象传递给函数时,也会调用拷贝构造函数。
5. explicit
关键字
explicit
关键字用于禁止构造函数的隐式转换,即只能显式调用该构造函数。通常用于避免不希望的类型转换,增强代码的安全性和可读性。
注意事项:
explicit
只能用于构造函数。- 使用
explicit
的构造函数不会参与隐式转换。
示例:
class MyClass {
public:
explicit MyClass(int x) {
std::cout << "Explicit Constructor Called with value " << x << std::endl;
}
};
int main() {
MyClass obj1(10); // 正确:显式调用
// MyClass obj2 = 20; // 错误:隐式转换被禁止
return 0;
}
扩展: 在这个例子中,MyClass
的构造函数使用了explicit
关键字。因此,隐式转换(如MyClass obj2 = 20;
)是不允许的,这样可以防止可能的错误转换。
6. 默认构造函数
如果类中没有定义任何构造函数,编译器会自动生成一个默认构造函数。但这个自动生成的构造函数并不会对数据成员进行初始化(数据成员可能是随机值)。
注意事项:
- 编译器自动生成的默认构造函数不会初始化数据成员。
- 显式定义了带参构造函数后,编译器将不再生成默认构造函数。如果仍需要默认构造函数,必须显式定义。
- 子类如果未显式定义默认构造函数,且父类没有默认构造函数,将导致子类对象创建失败。
示例:
class MyClass {
public:
int value;
MyClass() {
std::cout << "Default Constructor Called, value is not initialized." << std::endl;
}
MyClass(int x) : value(x) {
std::cout << "Parameterized Constructor Called, value is " << value << std::endl;
}
};
int main() {
MyClass obj1; // 调用显式定义的默认构造函数
MyClass obj2(10); // 调用带参数的构造函数
return 0;
}
扩展: 在这个例子中,如果没有显式定义默认构造函数,且只定义了带参数的构造函数,那么在创建MyClass
对象时,如使用MyClass obj1;
,将导致编译错误。因此,如果需要无参构造,一定要显式定义默认构造函数。
初始化列表
1. 初始化列表
初始化列表用于在构造函数中直接初始化类的数据成员,而不是在构造函数体内赋值。它位于构造函数的参数列表和函数体之间,用冒号 :
引导,后面跟随数据成员的初始化语句。
格式:
class MyClass {
private:
int data1;
int data2;
public:
MyClass(int x, int y) : data1(x), data2(y) {
// 构造函数体
}
};
扩展: 在这个例子中,MyClass
的构造函数使用了初始化列表来初始化数据成员data1
和data2
。这种方式直接在内存分配后进行初始化,效率较高,特别是对于复杂类型的成员。
2. 初始化列表的执行
**初始化列表的执行顺序是在对象内存分配之后,构造函数体执行之前,也就是分配内存的同时进行。**这意味着在进入构造函数体之前,数据成员已经通过初始化列表完成了初始化。
注意事项:
- 初始化顺序是根据数据成员在类中声明的顺序,而不是在初始化列表中的顺序。
- 对于多继承或复杂对象,初始化列表可以显著提高初始化效率。
示例:
class MyClass {
private:
int data1;
int data2;
public:
MyClass(int x, int y) : data1(x), data2(y) {
std::cout << "data1: " << data1 << ", data2: " << data2 << std::endl;
}
};
扩展: 这里,data1
和data2
在进入构造函数体之前已经被初始化。这种方法比在构造函数体内赋值更高效,因为它避免了默认构造后再赋值的过程。
3. 初始化列表主要用于初始化特殊数据
初始化列表在以下情况下尤为重要:
1) 常量数据成员
常量数据成员(const
)必须通过初始化列表来初始化,因为它们只能在对象创建时被赋值,不能在构造函数体内赋值。
示例:
class MyClass {
private:
const int data;
public:
MyClass(int x) : data(x) {
std::cout << "Constant data initialized to " << data << std::endl;
}
};
扩展: 在这个例子中,常量成员data
只能在初始化列表中赋值,因为常量的值一旦设定就不能改变。
2) 类中常引用数据成员
引用类型的成员也必须通过初始化列表初始化,因为引用必须在创建时绑定到一个对象或变量上,不能在构造函数体内重新赋值。
示例:
class MyClass {
private:
const int& ref;
public:
MyClass(int& r) : ref(r) {
std::cout << "Reference initialized to " << ref << std::endl;
}
};
扩展: 在这个例子中,ref
是一个引用成员,它在初始化列表中被绑定到外部的r
变量上。由于引用的性质,这个绑定只能在对象创建时完成。
3) 类中有其它类的对象作成员
如果类包含了其他类类型的成员对象,且这些对象没有默认构造函数,那么必须在初始化列表中指定它们的构造方式。
示例:
class MemberClass {
public:
MemberClass(int x) {
std::cout << "MemberClass Constructor Called with value " << x << std::endl;
}
};
class MyClass {
private:
MemberClass member;
public:
MyClass(int y) : member(y) {
std::cout << "MyClass Constructor Called" << std::endl;
}
};
扩展: 在这个例子中,MyClass
包含一个MemberClass
类型的成员对象member
。由于MemberClass
没有默认构造函数,所以必须在初始化列表中显式调用它的构造函数。
4) 父类的构造方式
在继承关系中,子类的构造函数通常需要通过初始化列表调用父类的构造函数。即使不显式调用,编译器也会尝试调用父类的默认构造函数。如果父类没有默认构造函数,则必须显式指定。
示例:
class Base {
public:
Base(int x) {
std::cout << "Base Constructor Called with value " << x << std::endl;
}
};
class Derived : public Base {
public:
Derived(int x) : Base(x) {
std::cout << "Derived Constructor Called" << std::endl;
}
};
扩展: 在这个例子中,Derived
类继承自Base
类,且Base
类只有一个带参数的构造函数,因此在Derived
类的初始化列表中必须调用Base
类的构造函数。
构造函数的顺序
1. 构造的顺序
在 C++ 中,类对象的构造是有严格顺序的,尤其当类中包含其他类对象作为成员时,理解这个构造顺序对确保代码的正确性至关重要。
1) 先构造其他类的对象
当一个类包含其他类对象作为其成员时,这些成员对象会在构造当前类对象之前被构造。构造的顺序是按照成员对象在类中声明的顺序,而不是初始化列表中的顺序。
- 成员对象的构造顺序:按照成员在类中的声明顺序进行,而不是按照初始化列表中的顺序。这意味着即使在初始化列表中将成员的顺序调换,构造顺序依然遵循声明的顺序。
示例:
class Member1 {
public:
Member1() {
std::cout << "Member1 Constructor Called" << std::endl;
}
};
class Member2 {
public:
Member2() {
std::cout << "Member2 Constructor Called" << std::endl;
}
};
class MyClass {
private:
Member1 m1;
Member2 m2;
public:
MyClass() : m2(), m1() {
std::cout << "MyClass Constructor Called" << std::endl;
}
};
扩展: 在这个例子中,尽管在初始化列表中m2
在前,m1
在后,Member1
对象m1
依然会先构造,因为它在类中先声明。输出顺序将是:
sql复制代码Member1 Constructor Called
Member2 Constructor Called
MyClass Constructor Called
这个例子说明了构造顺序是基于成员的声明顺序,而不是初始化列表的顺序。
2) 再执行本类构造
在所有成员对象构造完成后,才开始执行本类的构造。具体的执行步骤如下:
- 初始化列表:首先执行初始化列表中的成员初始化。这一步是在构造函数体执行之前完成的,它可以高效地直接初始化数据成员,避免了默认构造和赋值的过程。
- 构造函数体:在初始化列表执行完成后,开始执行构造函数体的代码。此时,所有成员对象和父类对象都已被正确构造,进入构造函数体时可以直接使用它们。
示例:
class MyClass {
private:
int value;
public:
MyClass(int v) : value(v) {
std::cout << "Initialization list sets value to " << value << std::endl;
value += 10; // 构造函数体内可以进一步操作
std::cout << "Constructor body modifies value to " << value << std::endl;
}
};
扩展: 在这个例子中,value
首先通过初始化列表被设置为传入的参数值。随后,构造函数体对value
进行了进一步的操作。在实际应用中,这种分工可以帮助提高代码的清晰度和效率:
Initialization list sets value to 5
Constructor body modifies value to 15
3. 扩展内容
- 父类构造顺序:在类的继承体系中,构造函数的执行顺序是先调用父类的构造函数,然后才是子类的构造函数。这确保了子类在构造时可以依赖父类已被正确构造的成员。
- 虚基类构造:如果存在虚基类(即多个派生类都继承自同一个基类),虚基类的构造函数在所有派生类的构造函数之前被调用。
示例:
class Base {
public:
Base() {
std::cout << "Base Constructor Called" << std::endl;
}
};
class Derived : public Base {
public:
Derived() {
std::cout << "Derived Constructor Called" << std::endl;
}
};
int main() {
Derived obj; // 输出顺序:Base Constructor Called -> Derived Constructor Called
return 0;
}
扩展: 在这个例子中,Derived
类的对象在构造时,首先调用的是Base
类的构造函数,然后才是Derived
类的构造函数。这表明了在继承结构中,基类总是在派生类之前构造。
浅拷贝和深拷贝
当类中有成员为指针时,必须谨慎处理对象的复制和销毁操作。为了更好地管理内存和避免常见的错误,了解如何正确实现构造函数、拷贝构造函数、析构函数,以及重载赋值操作符是至关重要的。
1. 浅拷贝(Shallow Copy)
概念
浅拷贝指的是在复制对象时,仅复制对象的基本数据类型成员和指针成员的地址值,而不是指针所指向的内存内容。换句话说,新对象的指针成员将指向与原对象相同的内存地址。
优点
- 节省内存:因为新对象的指针成员与原对象共享相同的内存,减少了内存开销。
- 效率高:浅拷贝的速度较快,因为仅复制了指针的地址值,而没有复制内存内容。
缺点
- 不安全:由于多个对象共享同一块内存,一旦一个对象释放了内存,其他对象会因访问无效内存而导致段错误(Segmentation Fault)。
- 容易引发悬空指针:当一个对象销毁时,其指针所指向的内存被释放,其他对象中的指针将变为悬空指针(Dangling Pointer),导致不可预测的行为。
示例代码
class ShallowCopyExample
{
private:
int *data;
public:
ShallowCopyExample(int value)
{
data = new int(value);
}
// 浅拷贝构造函数
ShallowCopyExample(const ShallowCopyExample &other) : data(other.data)
{
}
// 浅拷贝赋值操作符
ShallowCopyExample &operator=(const ShallowCopyExample &other)
{
if (this == &other)
return *this; // 防止自我赋值
data = other.data;
return *this;
}
~ShallowCopyExample()
{
delete data; // 可能导致多个对象试图释放同一块内存,导致错误
}
void print() const
{
std::cout << "Data: " << *data << std::endl;
}
};
void testShallowCopy()
{
ShallowCopyExample obj1(10);
ShallowCopyExample obj2 = obj1; // 浅拷贝
obj1.print();
obj2.print();
// 注意:obj1 和 obj2 共享同一个 data 指针,析构时会出现问题
}
2. 深拷贝(Deep Copy)
概念
深拷贝指的是在复制对象时,不仅复制对象的基本数据类型成员,还会为指针成员重新分配内存,并复制其所指向的内容。这样,新对象将拥有自己独立的内存空间,不与原对象共享。
优点
- 安全性:新对象拥有独立的内存,原对象和新对象的操作互不影响,避免了悬空指针和段错误的风险。
缺点
- 内存占用更多:由于每个对象都独立分配内存,内存开销较大。
- 效率较低:深拷贝需要复制整个内存内容,执行速度比浅拷贝慢。
示例代码
class Student
{
public:
string *name;
int age;
float grade;
Student()
{
cout << "Student()" << endl;
this->name = NULL;
this->age = 0;
this->grade = 0;
}
Student(string *name,int age,float grade)
{
cout << "Student(const string *name,int age,float grade)" << endl;
//如果参数name为null,则this->name也为空
if(NULL == name)
this->name = NULL;
//如果参数name不为null,则重新分配内存,并拷贝值
else
{
this->name = new string;
*this->name = *name;
}
this->age = age;
this->grade = grade;
}
void setName(string *name)
{
//如果之前this->name为null,
if(NULL == this->name)
{
//如果name为null;则直接赋值
if(NULL == name)
this->name = name;
//否则:为this->name要分配内存并拷贝值;
else
{
this->name = new string;
*this->name = *name;
}
}
//如果之前this->name不为null;
else
{
//如果name为nul;则释放this->name的内存,再设置为NULL
if(NULL == name)
{
delete this->name;
this->name = NULL;
}
//如果name不为null,则只拷贝值
else
*this->name = *name;
}
}
string& getName()
{
return *this->name;
}
void setAge(int age)
{
this->age = age;
}
int getAge()
{
return this->age;
}
void showStu()
{
if(this->name != NULL)
cout << *this->name << endl;
cout << this->age << " " << this->grade << endl;
}
};
Student getStu()
{
string temp = "zhangsan"; //局部变量
Student stu(&temp,22,99);
cout << "in getStu() stu.name = " << stu.name << endl;
stu.showStu();
return stu;
} //temp会被销毁掉
int main()
{
Student temp = getStu();
cout << "in main() stu.name = " << temp.name << endl;
temp.showStu(); //stu.name实际是野指针;
}
3. 其他注意事项
构造函数
- 负责为指针成员分配初始内存,确保对象的内存状态是正确的。
析构函数
- 负责释放指针成员所指向的内存,以避免内存泄漏。
拷贝构造函数
- 浅拷贝:仅复制指针的地址值。
- 深拷贝:为指针分配新的内存并复制内容。
赋值操作符重载
- 浅拷贝:复制指针的地址值。
- 深拷贝:释放已有内存,为指针分配新的内存并复制内容。
总结
浅拷贝和深拷贝的选择取决于具体的应用场景。在需要节省内存并确保多个对象可以共享资源的情况下,浅拷贝是一个合适的选择。然而,在需要确保对象独立性和避免内存管理问题的情况下,深拷贝则更加安全和稳妥。
析构函数详解
1. 析构函数的定义
- 函数名:析构函数的名称必须与类名相同,且在前面加上波浪符号
~
。 - 返回值:析构函数没有返回值,不能写
void
。 - 参数:析构函数不接受参数,因此不能进行重载。
- 数量:每个类只能有一个析构函数,不能定义多个。
class MyClass {
public:
~MyClass(); // 析构函数
};
2. 析构函数的作用
析构函数的主要作用是在对象销毁时执行一些清理工作,例如:
- 释放堆内存:在构造函数中通过
new
申请的内存,需要在析构函数中通过delete
释放。 - 关闭文件:如果类对象中包含了打开的文件,析构函数可以确保在对象销毁时关闭文件。
- 关闭网络连接:对于网络编程中的类对象,析构函数可以关闭在构造时建立的网络连接。
- 关闭数据库连接:同样地,析构函数也可以用于关闭数据库连接。
#include <iostream>
class MyClass {
private:
int* ptr;
public:
// 构造函数
MyClass(int value) {
ptr = new int(value);
std::cout << "Constructor: Memory allocated." << std::endl;
}
// 析构函数
~MyClass() {
delete ptr;
std::cout << "Destructor: Memory released." << std::endl;
}
};
int main() {
MyClass obj(10); // 创建对象,调用构造函数
// 作用域结束,自动调用析构函数
return 0;
}
3. 析构函数的执行
析构函数不需要显式调用,它会在以下两种情况下自动执行:
- 作用域结束:当对象的作用域结束时,编译器会自动调用析构函数。
- 使用
delete
或delete[]
操作符:当通过delete
或delete[]
销毁通过new
或new[]
动态分配的对象时,析构函数会自动执行。
#include <iostream>
class MyClass {
public:
MyClass() {
std::cout << "Constructor called." << std::endl;
}
~MyClass() {
std::cout << "Destructor called." << std::endl;
}
};
int main() {
MyClass* obj = new MyClass(); // 动态分配内存,调用构造函数
delete obj; // 释放内存,调用析构函数
return 0;
}
4. 析构的顺序
析构的顺序
- 析构顺序与构造顺序相反。
- 先构造的成员后析构,后构造的成员先析构。
拷贝构造函数
1. 什么是拷贝构造函数?
拷贝构造函数是一种特殊的构造函数,用于创建一个新的对象,并将已存在对象的值复制到新对象中。拷贝构造函数的函数名与类名相同,没有返回值。它的参数是本类对象的引用,通常使用常引用(const
)作为参数,以避免不必要的副本生成。
2. 拷贝构造函数的定义
拷贝构造函数的定义形式如下:
ClassName(const ClassName &obj);
其中,ClassName
是类名,obj
是要复制的对象的引用。
3. 拷贝构造函数的调用时机
拷贝构造函数会在以下几种情况下自动调用:
-
对象初始化:当一个新对象被创建并用另一个对象初始化时,拷贝构造函数被调用。例如:
Student stu2 = stu1; // 调用拷贝构造函数
-
对象作为函数参数传递:当对象作为函数参数以值传递方式传递时,拷贝构造函数被调用。例如:
void showStudent(Student temp); // 调用拷贝构造函数
-
对象作为函数返回值:当函数返回一个对象时,返回值的复制操作会调用拷贝构造函数。
4. 拷贝构造函数的作用
拷贝构造函数的主要作用是用已存在对象的值来初始化新对象。这对于对象的深拷贝(深复制)非常重要,尤其当类成员包含指针或动态分配的资源时。
5. 自定义拷贝构造函数的必要性
在某些情况下,编译器生成的默认拷贝构造函数可能无法正确地复制对象,特别是当对象包含指针或动态分配的资源时。因此,自定义拷贝构造函数是必要的,以确保资源被正确复制或共享。
6. 禁止拷贝构造函数的知识点
-
禁止拷贝构造函数:
- 禁止拷贝构造函数的方法是将拷贝构造函数声明为
private
,或者在 C++11 及其之后的标准中,使用delete
关键字来显式禁止。 - 目的: 禁止拷贝构造函数的主要目的是防止对象被意外拷贝。这在某些情况下有助于提高效率,尤其是在类的对象不应被复制的场景中(例如,管理系统资源或独占所有权的类)。
- 好处: 只能使用对象引用作参数,从而避免了对象的无谓复制,提高了代码的效率。
- 坏处: 无法使用拷贝构造函数,这可能导致对象无法存入需要对象复制的容器,如
std::vector
等 STL 容器。
- 禁止拷贝构造函数的方法是将拷贝构造函数声明为
-
禁止拷贝构造函数的实现示例:
class MyClass { public: MyClass() = default; MyClass(const MyClass&) = delete; // 禁止拷贝构造函数 MyClass& operator=(const MyClass&) = delete; // 禁止赋值操作符 };
移动构造函数
移动构造函数的概念
在C++11中引入了移动语义,以提高程序的性能。移动构造函数是移动语义的核心部分,用于“移动”对象的资源,而不是复制它们。这对于管理动态内存或其他资源(如文件句柄、网络连接等)尤为重要。
移动构造函数的定义
移动构造函数的作用是将资源从一个对象转移到另一个对象,而不进行资源的复制。移动构造函数通常具有以下特征:
- 接受一个右值引用参数(
T&&
),表示资源将从这个参数移动到新对象。 - 通常使用
noexcept
关键字,表示移动构造函数不会抛出异常。
class MyClass {
public:
int* data;
// 移动构造函数
MyClass(MyClass&& other) noexcept : data(other.data) {
other.data = nullptr; // 将源对象的指针置空
}
// 析构函数
~MyClass() {
delete[] data;
}
};
什么时候会调用移动构造函数
移动构造函数会在以下几种情况下被调用:
-
临时对象的转移: 当一个临时对象被用于初始化另一个对象时,编译器会优先使用移动构造函数。
MyClass createObject() { MyClass temp(10); return temp; } MyClass obj = createObject(); // 调用移动构造函数
-
使用
std::move
转换为右值引用: 当使用std::move
函数将一个对象转换为右值引用时,编译器会尝试调用移动构造函数。MyClass obj1(10); MyClass obj2(std::move(obj1)); // 调用移动构造函数
移动构造函数的实现细节
- 资源转移: 在移动构造函数中,资源(如指针)从源对象(右值引用)转移到目标对象。为了避免在析构时释放已经转移的资源,通常会将源对象的指针置为
nullptr
或其他默认值。 - 避免资源泄漏: 移动构造函数应确保在转移资源后,原对象不会尝试访问或释放这些资源。通常会在转移后将原对象的资源指针置空。
移动构造函数 vs 复制构造函数
- 复制构造函数会复制对象的数据,并创建与原对象独立的副本。
- 移动构造函数则转移对象的数据,不会创建新的副本,只是转移资源所有权。
示例
#include <iostream>
#include <vector>
class MyClass {
public:
int* data;
int size;
// 构造函数
MyClass(int s) : size(s), data(new int[s]) {
std::cout << "Constructed" << std::endl;
}
// 移动构造函数
MyClass(MyClass&& other) noexcept : data(other.data), size(other.size) {
other.data = nullptr; // 转移资源
other.size = 0;
std::cout << "Moved" << std::endl;
}
// 析构函数
~MyClass() {
delete[] data;
}
};
int main() {
MyClass a(10);
MyClass b(std::move(a)); // 触发移动构造函数
std::cout << "Size of a: " << a.size << std::endl;
std::cout << "Size of b: " << b.size << std::endl;
return 0;
}
什么时候应该定义移动构造函数?
- 当类中有动态分配的资源(如堆内存、文件句柄等)时,建议定义移动构造函数,以避免不必要的复制操作。
- 如果一个类的成员中含有已经实现了移动语义的类型(如
std::vector
、std::string
),编译器会自动为这个类生成默认的移动构造函数。
临时对象
临时对象临时对象:
-
- 临时对象是编译器在某些操作中自动创建的短生命周期对象。这些对象通常在函数返回值、类型转换或表达式求值中出现。
- 特点: 临时对象在表达式结束或函数调用结束时自动销毁。
-
直接使用临时对象调用函数:
- 直接使用临时对象调用函数可以减少不必要的内存开销,因为临时对象在使用后会被立即销毁,无需显式管理其生命周期。
- 例如,在函数调用时直接传递表达式结果,而不是先将结果赋值给一个变量,可以更有效地利用内存。
-
临时对象示例:
class MyClass { public: MyClass(int x) : data(x) {} void show() const { cout << data << endl; } private: int data; }; int main() { MyClass(10).show(); // 直接使用临时对象调用函数 return 0; }
友元
1. 概念
- 友元函数是一个可以访问类的私有成员和保护成员的函数,即使它不是该类的成员。
- 使用
friend
关键字来声明友元函数。
2. 友元函数的特点
- 访问权限:友元函数可以访问类的所有成员,包括私有成员和保护成员。
- 非成员函数:友元函数不是类的成员函数,但仍可以访问类的私有和保护成员。
- 定义位置:友元函数可以在类的外部定义,但必须在类的内部进行声明。
3. 友元函数的声明
class ClassName {
private:
int privateVar;
public:
friend void FriendFunction(ClassName& obj); // 声明友元函数
};
4. 友元的三种类型
- 一般函数作为友元函数:
- 如果一个普通函数被声明为某个类的友元函数,这个函数将能够访问该类的所有成员(包括私有和保护成员)。
- 这种方式通常用于实现操作符重载或者需要访问类私有数据的非成员函数。
- 某类的成员函数作为友元函数:
- 当一个类的成员函数被声明为另一个类的友元函数时,该成员函数可以访问另一个类的所有成员。
- 这在需要让一个类的成员函数与另一个类紧密协作时非常有用。
- 友元类:
- 如果一个类被声明为另一个类的友元类,那么这个类的所有成员函数都能访问另一个类的所有成员。
- 友元类的使用场景包括需要让两个类之间的合作更加紧密的情况下。
5. 友元函数的使用场景
- 操作重载:在操作符重载中,特别是当需要对两个不同类型的对象进行操作时,友元函数是非常有用的。
- 类的外部访问:当一个类希望允许某些外部函数访问其私有数据,而又不想将这些函数作为类的成员时,可以使用友元函数。
6. 友元函数的优缺点
- 优点:
- 提供了直接访问类的私有和保护成员的能力,有助于实现某些功能,如操作符重载。
- 缺点:
- **破坏了封装性:**友元函数违反了类的封装原则,外部函数可以直接访问私有数据。
- **增加了耦合度:**友元函数与类之间有紧密的耦合,可能导致代码维护困难。
- **解决方法:**提供公有的接口(get()、set()),而不是用友元。
内部类和局部类
1. 内部类(Nested Class)
- 概念: 内部类是在另一个类的定义中定义的类。它的声明和定义位于外部类的作用域内,因此被称为内部类。
- 访问权限:
- 内部类可以访问外部类的所有成员,包括私有和保护成员,但这种访问需要通过外部类的对象进行。
- 外部类对内部类的成员并没有特殊访问权限,外部类只能通过创建内部类的对象来访问其成员。
- 使用场景:
- 内部类通常用于逻辑上属于外部类的辅助类,或者是只在外部类中使用的类。
class Outer {
private:
int outerData;
public:
Outer() : outerData(5) {}
class Inner {
public:
void accessOuter(Outer& outer) {
// 通过外部类的对象访问其私有成员
cout << "Outer data: " << outer.outerData << endl;
}
};
};
int main() {
Outer outer;
Outer::Inner inner;
inner.accessOuter(outer);
return 0;
}
2. 局部类(Local Class)
- 概念: 局部类是在函数或代码块内定义的类。局部类的作用域仅限于定义它的函数或代码块之内。
- 访问权限:
- 局部类只能访问其所在的函数或代码块中的静态局部变量和外部类的静态成员。
- 它不能直接访问所在函数中的非静态局部变量(除非通过引用或指针)。
- 使用场景:
- 局部类通常用于需要在一个函数中封装特定逻辑或数据结构,而不需要在整个类中共享时。
void exampleFunction() {
static int staticVar = 10;
int localVar = 5;
class Local {
public:
void display() {
cout << "Static variable: " << staticVar << endl;
// 不能直接访问 localVar,因为它是非静态的局部变量
// cout << "Local variable: " << localVar << endl; // 错误
}
};
Local localObj;
localObj.display();
}
int main() {
exampleFunction();
return 0;
}
3. 内部类与局部类的比较
- 作用域:
- 内部类的作用域是外部类,它可以在外部类的任何成员函数中使用。
- 局部类的作用域仅限于定义它的函数或代码块,无法在其他地方使用。
- 访问能力:
- 内部类可以访问外部类的所有成员,但需要通过外部类的对象。
- 局部类只能访问其所在函数或代码块的静态局部变量,不能访问非静态的局部变量。
4. 优缺点
- 内部类的优点:
- 有助于封装逻辑相关的类结构,避免污染全局命名空间。
- 局部类的优点:
- 提供了在函数级别封装类定义的能力,使类的使用范围更加局限化,避免与其他类产生冲突。
- 缺点:
- 内部类和局部类都可能增加代码的复杂性,尤其是当类嵌套较深时,代码可读性可能下降。