一.类的默认六个成员
一、构造函数
- 作用:用于在创建对象时初始化对象的数据成员。
- 特点:与类名相同,没有返回值类型。可以有多个重载形式,以满足不同的初始化需求
class MyClass {
public:
MyClass() {
// 默认构造函数实现
}
MyClass(int value) {
// 带参数的构造函数实现
}
};
二、析构函数
- 作用:在对象被销毁时自动调用,用于释放对象所占用的资源。
- 特点:与类名相同,前面加上波浪线(~),没有返回值类型和参数。例如:
class MyClass {
public:
~MyClass() {
// 析构函数实现
}
};
三、拷贝构造函数
- 作用:用一个已存在的对象来初始化另一个同类对象。
- 特点:参数是该类对象的常量引用。例如:
class MyClass {
public:
MyClass(const MyClass& other) {
// 拷贝构造函数实现
}
};
四、赋值运算符重载函数
- 作用:将一个对象赋值给另一个同类对象。
- 特点:返回该类对象的引用,参数是该类对象的常量引用。例如:
class MyClass {
public:
MyClass& operator=(const MyClass& other) {
// 赋值运算符重载实现
return *this;
}
};
五、取地址运算符重载函数
- 作用:返回对象的地址。
- 特点:返回该类对象的指针,没有参数。例如:
class MyClass {
public:
MyClass* operator&() {
// 取地址运算符重载实现
return this;
}
};
六、取常量地址运算符重载函数
- 作用:返回对象的常量地址。
- 特点:返回该类对象的常量指针,没有参数。例如:
class MyClass {
public:
const MyClass* operator&() const {
// 取常量地址运算符重载实现
return this;
}
};
二.构造函数
1.构造函数体赋值
在 C++ 中,构造函数体赋值是一种在构造函数体内对数据成员进行赋值的方式。
例如:
class MyClass {
private:
int data;
public:
MyClass(int value) {
data = value;
}
};
在这个例子中,构造函数MyClass(int value)
接受一个参数value
,在构造函数体内将这个参数的值赋给数据成员data
。
构造函数体赋值与使用初始化列表进行初始化有所不同。使用初始化列表可以在对象创建时直接初始化数据成员,而构造函数体赋值是在构造函数执行到赋值语句时才进行赋值操作。一般来说,对于简单数据类型,两者的效果可能差别不大,但对于包含复杂对象的数据成员,使用初始化列表可以避免不必要的临时对象的创建和销毁,效率更高。
例如,如果有一个类OtherClass
,在MyClass
中有一个OtherClass
类型的数据成员:
class OtherClass {
// 假设 OtherClass 的具体实现
};
class MyClass {
private:
int data;
OtherClass obj;
public:
MyClass(int value) : obj(OtherClass()) {
data = value;
}
};
在这个例子中,使用初始化列表初始化obj
,避免了先使用默认构造函数创建obj
,再通过赋值来修改它的过程
2.初始化列表
在 C++ 中,构造函数初始化列表是一种在构造函数定义中对数据成员进行初始化的方式。
一、语法形式
构造函数初始化列表紧跟在构造函数参数列表之后,以冒号(:)开头,接着是一系列成员初始化表达式,每个表达式由成员变量名和初始值组成,用逗号分隔。例如:
class MyClass {
private:
int num;
double value;
public:
MyClass(int n, double v) : num(n), value(v) {
// 构造函数体可以为空,因为成员已经在初始化列表中初始化
}
};
二、作用和优势
- 明确初始化顺序:对于类中有多个数据成员的情况,使用初始化列表可以明确指定各个成员的初始化顺序,避免因成员初始化顺序不明确而导致的错误。初始化的顺序与成员在类中声明的顺序一致,而与初始化列表中的顺序无关。
- 提高效率:对于一些需要进行初始化的常量成员、引用成员和没有默认构造函数的类类型成员,必须使用初始化列表进行初始化。否则,编译器会报错。对于具有复杂构造函数的类类型成员,使用初始化列表可以避免调用默认构造函数后再进行赋值操作,从而提高效率。
- 例如,对于常量成员变量
class MyClass {
private:
const int constant;
public:
MyClass(int n) : constant(n) {
// 常量成员只能在初始化列表中初始化
}
};
- 对于引用成员变量:
class MyClass {
private:
int& ref;
public:
MyClass(int& r) : ref(r) {
// 引用成员也只能在初始化列表中初始化
}
};
- 对于没有默认构造函数的类类型成员:
class OtherClass {
public:
OtherClass(int n);
// 没有默认构造函数
};
class MyClass {
private:
OtherClass obj;
public:
MyClass(int n) : obj(n) {
// 使用初始化列表初始化没有默认构造函数的成员
}
};
三、使用场景
- 当类中有多个构造函数时,可以在每个构造函数中使用初始化列表来初始化成员变量,以保证不同的构造函数都能正确地初始化成员。
- 在继承关系中,派生类的构造函数可以使用初始化列表来调用基类的构造函数,初始化基类部分的成员。例如:
class Base {
public:
Base(int n);
};
class Derived : public Base {
private:
int derivedData;
public:
Derived(int n, int d) : Base(n), derivedData(d) {
// 初始化列表中先调用基类构造函数,再初始化派生类成员
}
};
3.explicit关键字
在 C++ 中,explicit
关键字用于修饰构造函数,以防止隐式类型转换。
一、作用
当一个构造函数被声明为explicit
时,它不能用于隐式类型转换。这意味着,不能在需要目标类型的地方自动调用该构造函数进行类型转换。
例如:
class MyClass {
public:
int value;
explicit MyClass(int n) : value(n) {}
};
在这个例子中,构造函数被声明为explicit
,所以下面的代码将无法编译:
void func(MyClass obj);
int main() {
int num = 10;
func(num); // 错误,不能隐式地将 int 转换为 MyClass
return 0;
}
如果构造函数没有被声明为explicit
,那么上面的代码是合法的,编译器会自动调用构造函数将num
转换为MyClass
类型的对象并传递给func
函数。
二、优点
- 避免意外的类型转换:使用
explicit
可以防止意外的类型转换,提高代码的安全性和可读性。例如,如果一个类的构造函数可以隐式地将其他类型转换为该类类型,那么在一些复杂的代码中可能会导致意外的结果,难以调试。 - 明确类型转换意图:当需要进行类型转换时,必须显式地调用构造函数,这样可以使代码更加清晰地表达程序员的意图。
三、注意事项
explicit
关键字只对单参数的构造函数有效。对于多参数的构造函数,即使没有使用explicit
修饰,也不会进行隐式类型转换。explicit
关键字不会影响显式类型转换。可以使用显式类型转换语法(如static_cast
)来进行类型转换。
三.析构函数
在 C++ 中,析构函数是一种特殊的成员函数,当对象被销毁时自动调用。
一、作用
- 释放资源:析构函数主要用于释放对象在生命周期中所占用的资源,如动态分配的内存、打开的文件、网络连接等。如果对象在生命周期中获取了一些系统资源,而在对象销毁时不释放这些资源,可能会导致资源泄漏。
- 执行清理操作:除了释放资源,析构函数还可以执行一些其他的清理操作,如关闭数据库连接、停止线程等。
二、语法特点
- 函数名:析构函数的名称与类名相同,前面加上波浪线(~)。例如,对于类
MyClass
,其析构函数的名称为~MyClass
。 - 没有返回值类型和参数:析构函数不能有返回值类型,也不能接受参数。
三、示例
class MyClass {
private:
int* data;
public:
MyClass() {
data = new int[10];
}
~MyClass() {
delete[] data;
// 释放动态分配的内存
}
};
在这个例子中,类MyClass
的构造函数中动态分配了一块内存,在析构函数中使用delete[]
释放了这块内存,以避免内存泄漏。
四、调用时机
- 局部对象:当局部对象超出其作用域时,析构函数会被自动调用。例如:
void func() {
MyClass obj;
//...
}
// 当 func 函数执行完毕,obj 对象超出作用域,其析构函数被调用。
- 动态分配的对象:当使用
delete
运算符释放动态分配的对象时,析构函数会被自动调用。例如:
MyClass* ptr = new MyClass();
//...
delete ptr;
// 此时 ptr 所指向的对象的析构函数被调用。
- 作为栈对象的成员对象:当包含成员对象的对象被销毁时,成员对象的析构函数会被自动调用。例如:
class Container {
private:
MyClass member;
public:
//...
};
void func() {
Container obj;
//...
}
// 当 func 函数执行完毕,obj 对象超出作用域,obj 中的 member 对象的析构函数被调用。
- 作为堆对象的成员对象:当使用
delete
运算符释放包含成员对象的动态分配对象时,成员对象的析构函数会被自动调用。例如:
class Container {
private:
MyClass* member;
public:
Container() {
member = new MyClass();
}
~Container() {
delete member;
}
};
void func() {
Container* ptr = new Container();
//...
delete ptr;
// 此时 ptr 所指向的对象被销毁,其成员对象 member 的析构函数被调用。
}
总之,析构函数在 C++ 中起着重要的作用,它可以确保对象在生命周期结束时正确地释放资源和执行清理操作。在编写类时,应该根据需要合理地定义析构函数,以避免资源泄漏和其他潜在的问题。
四.拷贝构造函数
在 C++ 中,拷贝构造函数是一种特殊的构造函数,用于创建一个新对象,并用另一个已存在的同类型对象来初始化它。
一、作用
- 对象初始化:当使用一个已存在的对象来初始化另一个对象时,拷贝构造函数会被自动调用。例如:
class MyClass {
public:
MyClass(const MyClass& other) {
// 拷贝构造函数实现
}
};
int main() {
MyClass obj1;
MyClass obj2 = obj1; // 调用拷贝构造函数
return 0;
}
- 函数参数传递:当一个对象作为函数参数按值传递时,会调用拷贝构造函数来创建函数参数的副本。例如:
void func(MyClass obj) {
// 函数体
}
int main() {
MyClass obj1;
func(obj1); // 调用拷贝构造函数创建函数参数的副本
return 0;
}
- 函数返回值:当一个函数按值返回一个对象时,会调用拷贝构造函数来创建返回值的副本。例如:
MyClass func() {
MyClass obj;
return obj; // 调用拷贝构造函数创建返回值的副本
}
int main() {
MyClass obj1 = func();
return 0;
}
二、语法特点
- 函数名:拷贝构造函数的名称与类名相同,参数是该类对象的常量引用。例如,对于类
MyClass
,其拷贝构造函数的原型为MyClass(const MyClass& other)
。 - 常量引用参数:使用常量引用作为参数可以避免不必要的对象复制,提高效率。同时,也可以接受常量对象作为参数。
- 没有返回值类型:拷贝构造函数没有返回值类型。
三、默认拷贝构造函数
如果程序员没有显式地定义拷贝构造函数,C++ 编译器会自动生成一个默认的拷贝构造函数。默认的拷贝构造函数会逐个成员地进行浅拷贝,即将源对象的每个成员的值复制到目标对象的相应成员中。
class MyClass {
private:
int data;
public:
MyClass(int n) : data(n) {}
};
int main() {
MyClass obj1(10);
MyClass obj2 = obj1; // 调用默认拷贝构造函数
return 0;
}
在这个例子中,编译器生成的默认拷贝构造函数会将obj1
的data
成员的值复制到obj2
的data
成员中。
四、自定义拷贝构造函数
在某些情况下,默认的拷贝构造函数可能不能满足需求,这时程序员可以自定义拷贝构造函数来实现特定的拷贝行为。例如,如果类中有动态分配的内存或其他资源,需要在拷贝构造函数中进行深拷贝,以避免资源泄漏和错误。
class MyClass {
private:
int* data;
public:
MyClass(int n) {
data = new int[n];
}
~MyClass() {
delete[] data;
}
MyClass(const MyClass& other) {
int size = sizeof(other.data) / sizeof(other.data[0]);
data = new int[size];
for (int i = 0; i < size; i++) {
data[i] = other.data[i];
}
}
};
int main() {
MyClass obj1(10);
MyClass obj2 = obj1; // 调用自定义拷贝构造函数
return 0;
}
在这个例子中,自定义的拷贝构造函数实现了深拷贝,确保了两个对象拥有独立的动态分配内存,避免了资源泄漏。
五.赋值运算符重载
在 C++ 中,赋值运算符重载是一种特殊的函数,用于为一个已存在的对象赋予新的值。它允许你自定义对象之间的赋值行为。
一、作用
- 自定义赋值行为:当使用赋值运算符(=)将一个对象赋值给另一个同类型的对象时,赋值运算符重载函数会被自动调用。通过重载这个运算符,你可以定义特定的赋值逻辑,以满足类的特定需求。
- 资源管理:如果类中包含动态分配的内存或其他资源,赋值运算符重载可以确保正确地管理这些资源。例如,在赋值过程中进行深拷贝,避免资源泄漏或重复释放资源。
二、语法特点
- 函数名:赋值运算符重载函数的名称是
operator=
,返回值类型是该类的引用。参数是该类对象的常量引用。例如,对于类MyClass
,其赋值运算符重载函数的原型为MyClass& operator=(const MyClass& other)
。 - 返回值类型为引用:返回值类型为类的引用可以实现连续赋值的功能。例如,
obj1 = obj2 = obj3
。 - 常量引用参数:使用常量引用作为参数可以避免不必要的对象复制,提高效率。同时,也可以接受常量对象作为赋值的源对象。
三、默认赋值运算符
如果程序员没有显式地定义赋值运算符重载函数,C++ 编译器会自动生成一个默认的赋值运算符。默认的赋值运算符会逐个成员地进行浅拷贝,即将源对象的每个成员的值复制到目标对象的相应成员中。
例如:
class MyClass {
private:
int data;
public:
MyClass(int n) : data(n) {}
};
int main() {
MyClass obj1(10);
MyClass obj2(20);
obj2 = obj1; // 调用默认赋值运算符
return 0;
}
在这个例子中,编译器生成的默认赋值运算符会将obj1
的data
成员的值复制到obj2
的data
成员中。
四、自定义赋值运算符重载函数
在某些情况下,默认的赋值运算符可能不能满足需求,这时程序员可以自定义赋值运算符重载函数来实现特定的赋值行为。例如,如果类中有动态分配的内存或其他资源,需要在赋值运算符重载函数中进行深拷贝,以避免资源泄漏和错误。
class MyClass {
private:
int* data;
public:
MyClass(int n) {
data = new int[n];
}
~MyClass() {
delete[] data;
}
MyClass& operator=(const MyClass& other) {
if (this!= &other) {
delete[] data;
int size = sizeof(other.data) / sizeof(other.data[0]);
data = new int[size];
for (int i = 0; i < size; i++) {
data[i] = other.data[i];
}
}
return *this;
}
};
int main() {
MyClass obj1(10);
MyClass obj2(20);
obj2 = obj1; // 调用自定义赋值运算符重载函数
return 0;
}
在这个例子中,自定义的赋值运算符重载函数实现了深拷贝,确保了两个对象拥有独立的动态分配内存,避免了资源泄漏。
六.const成员函数
在 C++ 中,const
成员函数是一种特殊的成员函数,它承诺在函数内部不会修改类的任何数据成员。
一、作用
- 保证数据的完整性:通过声明成员函数为
const
,可以向调用者保证该函数不会修改对象的状态。这对于那些只用于读取对象数据而不进行修改的函数非常有用,可以防止意外的修改。 - 支持常量对象:常量对象只能调用
const
成员函数。如果一个类没有提供const
版本的成员函数,那么常量对象将无法调用任何成员函数,这将限制常量对象的使用。 - 提高代码的可读性:声明成员函数为
const
可以使代码更加清晰地表达函数的意图。调用者可以通过函数的声明快速了解该函数是否会修改对象的状态。
二、语法特点
- 声明方式:在成员函数的声明和定义中,在函数参数列表后面加上
const
关键字。例如:
class MyClass {
private:
int data;
public:
MyClass(int value) : data(value) {}
int getData() const {
return data;
}
};
const
成员函数与非const
成员函数的重载:可以同时提供const
和非const
版本的成员函数,以满足不同的调用需求。例如:
class MyClass {
private:
int data;
public:
MyClass(int value) : data(value) {}
int getData() const {
return data;
}
void setData(int value) {
data = value;
}
};
在这个例子中,getData
是const
成员函数,只能用于读取data
的值,而setData
是非const
成员函数,可以用于修改data
的值。
三、注意事项
-
const
成员函数内部不能修改非mutable
数据成员:在const
成员函数内部,只能读取数据成员的值,不能修改它们。但是,可以修改mutable
数据成员,因为mutable
数据成员不受const
约束。 -
const
成员函数可以被非const
对象和const
对象调用:非const
对象可以调用const
和非const
版本的成员函数,而const
对象只能调用const
版本的成员函数。
例如:
class MyClass {
private:
int data;
mutable int mutableData;
public:
MyClass(int value) : data(value), mutableData(0) {}
int getData() const {
return data;
}
void setData(int value) {
data = value;
}
void setMutableData(int value) const {
mutableData = value;
}
};
int main() {
MyClass obj(10);
const MyClass constObj(20);
std::cout << obj.getData() << std::endl;
obj.setData(30);
std::cout << obj.getData() << std::endl;
std::cout << constObj.getData() << std::endl;
// constObj.setData(40); // 错误,const 对象不能调用非 const 成员函数
constObj.setMutableData(50);
return 0;
}
总之,const
成员函数在 C++ 中是一种非常有用的特性,它可以保证数据的完整性,支持常量对象,提高代码的可读性。在设计类时,应该根据需要合理地使用const
成员函数,以提高代码的质量和可维护性。
七.取地址及const取地址操作符重载
在 C++ 中,可以重载取地址运算符(&
)和常量取地址运算符(const &
)。
一、作用
- 自定义对象的地址获取方式:通过重载这两个运算符,可以控制如何获取对象的地址。这在一些特殊的场景下非常有用,例如当对象的地址获取方式与普通情况不同时,或者需要对地址的获取进行一些额外的处理时。
- 支持智能指针等高级特性:在实现智能指针等高级特性时,可能需要重载取地址运算符来提供正确的地址获取方式。例如,自定义的智能指针类可能需要重载取地址运算符来返回所管理对象的地址。
二、语法特点
-
函数名和参数:
- 取地址运算符重载函数的名称是
operator&
,没有参数,返回类型是类的指针(例如MyClass*
)。 - 常量取地址运算符重载函数的名称是
operator&
,带有const
修饰符,没有参数,返回类型是常量类的指针(例如const MyClass*
)。
- 取地址运算符重载函数的名称是
-
实现方式:
- 在类的内部定义这两个运算符重载函数,实现特定的地址获取逻辑。
- 通常,这两个函数会返回指向当前对象的指针。
例如:
class MyClass {
private:
int data;
public:
MyClass(int value) : data(value) {}
MyClass* operator&() {
std::cout << "Overloaded & operator called." << std::endl;
return this;
}
const MyClass* operator&() const {
std::cout << "Overloaded const & operator called." << std::endl;
return this;
}
};
三、注意事项
- 重载的取地址运算符应该与普通的地址获取行为保持一致:重载的取地址运算符应该返回正确的地址,并且在行为上与普通的地址获取方式相似。如果重载的运算符行为与预期不符,可能会导致代码的错误和不可预测的结果。
- 考虑常量对象的情况:在重载常量取地址运算符时,要确保返回的是常量指针,以保证常量对象的地址不能被修改。
- 避免过度复杂的实现:取地址运算符通常应该是简单和高效的,避免在重载函数中进行复杂的计算或操作,以免影响性能。
例如:
int main() {
MyClass obj(10);
MyClass* ptr1 = &obj;
const MyClass obj2(20);
const MyClass* ptr2 = &obj2;
return 0;
}
在这个例子中,当获取obj
和obj2
的地址时,会分别调用重载的取地址运算符和常量取地址运算符,并输出相应的信息。
总之,重载取地址运算符和常量取地址运算符可以提供自定义的地址获取方式,但在使用时要注意保持行为的一致性和正确性,避免过度复杂的实现。
八.Static成员
1.概念
在 C++ 中,static
关键字有多种用途,主要用于定义静态成员和静态函数。
一、静态成员变量
-
存储方式和生命周期:
- 静态成员变量被存储在静态存储区,不属于任何一个对象实例。它在程序开始执行时被创建,在程序结束时才被销毁,其生命周期贯穿整个程序的运行。
- 无论创建多少个类的对象,静态成员变量只有一份副本。
-
访问方式:
- 可以通过类名和作用域解析运算符(
::
)直接访问静态成员变量,无需创建对象。例如:ClassName::staticVariable
。 - 也可以通过对象来访问静态成员变量,但这种方式不推荐,因为它没有体现出静态成员变量的独立性。例如:
object.staticVariable
。
- 可以通过类名和作用域解析运算符(
-
初始化:
- 静态成员变量必须在类外部进行初始化,且只能初始化一次。初始化的语法为:
数据类型 类名::静态成员变量名 = 初始值;
。
- 静态成员变量必须在类外部进行初始化,且只能初始化一次。初始化的语法为:
例如:
class MyClass {
public:
static int staticVariable;
};
int MyClass::staticVariable = 0;
int main() {
MyClass::staticVariable = 10;
std::cout << MyClass::staticVariable << std::endl;
return 0;
}
二、静态成员函数
-
特点:
- 静态成员函数属于整个类,而不是某个特定的对象。它可以直接通过类名和作用域解析运算符来调用,无需创建对象。
- 静态成员函数不能访问非静态成员变量和非静态成员函数,因为它不与特定的对象实例相关联。但是,它可以访问静态成员变量和其他静态成员函数。
-
用途:
- 通常用于实现与类相关的全局功能,而不依赖于特定的对象状态。例如,用于统计类的实例数量、提供通用的工具函数等。
例如:
class MyClass {
public:
static int staticVariable;
static void staticFunction() {
std::cout << "Static function called. Static variable: " << staticVariable << std::endl;
}
};
int MyClass::staticVariable = 0;
int main() {
MyClass::staticFunction();
return 0;
}
三、静态局部变量
-
存储方式和生命周期:
- 静态局部变量存储在静态存储区,与全局变量的存储位置相同。它在程序第一次执行到其声明处时被初始化,并且只初始化一次。
- 静态局部变量的生命周期从初始化开始,直到程序结束。即使函数调用结束,静态局部变量的值也会被保留。
-
作用域:
- 静态局部变量的作用域仅限于声明它的函数内部。它在函数外部不可见,不能被其他函数直接访问。
例如:
void myFunction() {
static int staticLocalVariable = 0;
std::cout << "Static local variable: " << staticLocalVariable << std::endl;
staticLocalVariable++;
}
int main() {
myFunction(); // 输出:Static local variable: 0
myFunction(); // 输出:Static local variable: 1
myFunction(); // 输出:Static local variable: 2
return 0;
}
总之,static
关键字在 C++ 中提供了一种方式来定义与类或函数相关的静态成员和局部变量,它们具有特定的存储方式、生命周期和访问规则,可以满足不同的编程需求。
2.特性
在 C++ 中,static
关键字具有以下主要特性:
一、静态成员变量特性
-
单一存储:
- 无论创建多少个类的对象,静态成员变量只有一个实例,存储在静态存储区。所有对象共享这一变量。
- 例如,一个表示学生的类,用静态成员变量来记录学生总数,无论创建多少个学生对象,这个总数只有一份。
-
生命周期长:
- 静态成员变量在程序开始执行时创建,在程序结束时销毁,其生命周期贯穿整个程序的运行。
- 不像普通成员变量,随着对象的创建和销毁而存在和消失。
-
类内声明,类外初始化:
- 在类中声明静态成员变量,但必须在类外部进行初始化。初始化的语法为 “数据类型 类名::静态成员变量名 = 初始值;”。
- 这是因为静态成员变量的存储位置与普通成员变量不同,需要在全局作用域进行初始化。
-
独立于对象的访问:
- 可以通过类名和作用域解析运算符(
::
)直接访问静态成员变量,无需创建对象实例。 - 例如 “ClassName::staticVariable”。也可以通过对象来访问,但这种方式不直观,不推荐使用。
- 可以通过类名和作用域解析运算符(
二、静态成员函数特性
-
属于类而非对象:
- 静态成员函数是类的一部分,不与特定的对象实例相关联。它可以在没有对象的情况下被调用。
- 例如,一个工具类中的静态成员函数,用于执行一些通用的操作,不需要依赖于类的具体对象。
-
不能访问非静态成员:
- 静态成员函数不能直接访问非静态成员变量和非静态成员函数。这是因为静态成员函数没有 “this” 指针,不知道具体的对象实例。
- 如果需要访问非静态成员,通常需要通过对象实例作为参数传递给静态成员函数。
-
常用于通用功能:
- 通常用于实现与类相关的全局功能,不依赖于对象的状态。例如,用于统计类的实例数量、提供通用的工具函数等。
三、静态局部变量特性
-
局部作用域,全局生命周期:
- 静态局部变量的作用域仅限于声明它的函数内部,但它的生命周期从程序第一次执行到其声明处时开始,直到程序结束。
- 与普通局部变量不同,普通局部变量在函数调用结束后就被销毁,而静态局部变量的值会被保留。
-
只初始化一次:
- 静态局部变量在程序第一次执行到其声明处时被初始化,并且只初始化一次。后续的函数调用不会再次初始化它。
- 这使得静态局部变量可以用于在函数调用之间保持状态。
例如:
class MyClass {
public:
static int staticMemberVariable;
static void staticMemberFunction() {
std::cout << "Static member function called. Static variable: " << staticMemberVariable << std::endl;
}
};
int MyClass::staticMemberVariable = 0;
void localFunction() {
static int staticLocalVariable = 0;
std::cout << "Static local variable: " << staticLocalVariable << std::endl;
staticLocalVariable++;
}
int main() {
MyClass::staticMemberFunction();
localFunction();
localFunction();
localFunction();
return 0;
}
九.友元
1.友元函数
在 C++ 中,友元函数是一种特殊的函数,它可以访问类的私有成员和保护成员,尽管它不是类的成员函数。
一、作用
- 提供灵活性:友元函数允许在不破坏类的封装性的前提下,为特定的操作提供更灵活的访问权限。例如,某些操作可能需要直接访问类的内部数据结构,但又不适合作为类的成员函数。
- 提高效率:在某些情况下,友元函数可以避免通过成员函数调用的开销,提高程序的效率。例如,对于一些频繁执行的操作,如果可以直接访问类的内部数据,而不需要通过成员函数的接口,可以减少函数调用的开销。
二、语法特点
- 声明位置:友元函数在类的声明中进行声明,使用关键字 “friend”。例如:
class MyClass {
private:
int privateData;
public:
MyClass(int data) : privateData(data) {}
friend void friendFunction(MyClass& obj);
};
- 定义位置:友元函数的定义可以在类的外部进行,与普通函数的定义方式相同。例如:
void friendFunction(MyClass& obj) {
std::cout << "Accessing private data: " << obj.privateData << std::endl;
}
三、注意事项
- 谨慎使用:友元函数破坏了类的封装性,因为它可以直接访问类的私有成员。因此,应该谨慎使用友元函数,只有在确实需要的情况下才使用。
- 可能导致依赖关系:友元函数与它所访问的类之间存在依赖关系。如果类的内部实现发生变化,可能会影响到友元函数的正确性。因此,在修改类的实现时,需要注意友元函数是否也需要相应的修改。
- 不能继承:友元函数不是类的成员函数,因此不能被继承。如果一个派生类需要访问基类的私有成员,不能通过继承基类的友元函数来实现。
例如:
class Base {
private:
int privateData;
public:
Base(int data) : privateData(data) {}
friend void friendFunction(Base& obj);
};
void friendFunction(Base& obj) {
std::cout << "Accessing private data of Base: " << obj.privateData << std::endl;
}
class Derived : public Base {
public:
Derived(int data) : Base(data) {}
};
int main() {
Base baseObj(10);
friendFunction(baseObj);
Derived derivedObj(20);
// friendFunction(derivedObj); // 错误,友元函数不能继承
return 0;
}
总之,友元函数是 C++ 中一种特殊的机制,它可以提供对类的私有成员的访问权限,但也带来了一些潜在的问题。在使用友元函数时,应该谨慎考虑其必要性和影响,以确保程序的正确性和可维护性。
2.友元类
在 C++ 中,友元类是一种特殊的类关系,它允许一个类访问另一个类的私有成员和保护成员。
一、作用
- 提供更高级别的访问权限:友元类可以打破类的封装性,使得一个类能够直接访问另一个类的私有成员。这在某些情况下非常有用,例如当两个类之间存在紧密的合作关系,需要共享一些内部数据或操作时。
- 实现特定的设计模式:友元类可以用于实现一些设计模式,如访问者模式。在访问者模式中,访问者类需要访问被访问对象的内部结构,而友元关系可以提供这种访问权限。
二、语法特点
- 声明位置:在一个类的声明中,使用关键字 “friend” 声明另一个类为友元类。例如:
class ClassB;
class ClassA {
private:
int privateDataA;
public:
ClassA(int data) : privateDataA(data) {}
friend class ClassB;
};
class ClassB {
public:
void accessClassA(ClassA& obj) {
std::cout << "Accessing private data of ClassA: " << obj.privateDataA << std::endl;
}
};
- 访问权限:友元类的成员函数可以直接访问另一个类的私有成员和保护成员,就像访问自己的成员一样。
三、注意事项
- 谨慎使用:友元类破坏了类的封装性,因此应该谨慎使用。只有在确实需要的情况下才使用友元类,并且应该尽量减少对私有成员的直接访问,以保持类的封装性和可维护性。
- 可能导致依赖关系:友元类之间存在依赖关系。如果一个类的内部实现发生变化,可能会影响到友元类的正确性。因此,在修改类的实现时,需要注意友元类是否也需要相应的修改。
- 不能继承:友元关系不能被继承。如果一个派生类需要访问基类的私有成员,不能通过继承基类的友元类来实现。
例如:
class Base;
class Derived;
class Base {
private:
int privateData;
public:
Base(int data) : privateData(data) {}
friend class Derived;
};
class Derived : public Base {
public:
Derived(int data) : Base(data) {}
void accessBase() {
std::cout << "Accessing private data of Base: " << privateData << std::endl;
}
};
int main() {
Derived derivedObj(10);
derivedObj.accessBase();
return 0;
}
十.内部类
在 C++ 中,内部类(嵌套类)是在另一个类的内部定义的类。
一、作用
- 封装和信息隐藏:内部类可以将相关的类紧密地封装在一起,提高代码的封装性。外部类可以控制对内部类的访问,只在需要的地方暴露内部类的功能。
- 实现特定的设计模式:内部类可以用于实现一些设计模式,如适配器模式、装饰器模式等。通过将特定的功能封装在内部类中,可以使代码更加清晰和易于维护。
- 代码组织和可读性:内部类可以使代码更加组织有序,提高可读性。将相关的功能放在一起,可以更容易理解代码的结构和逻辑。
二、语法特点
- 定义位置:内部类在外部类的内部进行定义。例如:
class OuterClass {
public:
OuterClass() {}
void outerFunction() {
std::cout << "Outer function called." << std::endl;
}
class InnerClass {
public:
InnerClass() {}
void innerFunction() {
std::cout << "Inner function called." << std::endl;
}
};
};
- 访问权限:内部类可以访问外部类的成员,包括私有成员。外部类也可以通过内部类的对象来访问内部类的成员。
- 对象创建:要创建内部类的对象,需要在外部类的作用域内进行。例如:
int main() {
OuterClass outerObj;
OuterClass::InnerClass innerObj;
innerObj.innerFunction();
outerObj.outerFunction();
return 0;
}
三、注意事项
- 命名冲突:在定义内部类时,需要注意命名冲突的问题。内部类的名称不能与外部类或其他作用域中的名称相同。
- 作用域和生命周期:内部类的作用域仅限于外部类的内部。内部类的对象的生命周期也受到外部类对象的影响。
- 复杂性:使用内部类可能会增加代码的复杂性。在使用内部类时,需要仔细考虑其必要性和影响,以确保代码的可读性和可维护性。
例如:
class Container {
public:
Container() {}
void addItem(int item) {
items.push_back(item);
}
class Iterator {
public:
Iterator(Container& container) : container(container), index(0) {}
bool hasNext() {
return index < container.items.size();
}
int next() {
return container.items[index++];
}
private:
Container& container;
int index;
};
private:
std::vector<int> items;
};
int main() {
Container container;
container.addItem(1);
container.addItem(2);
container.addItem(3);
Container::Iterator iterator(container);
while (iterator.hasNext()) {
std::cout << iterator.next() << std::endl;
}
return 0;
}
在这个例子中,Container
类内部定义了一个Iterator
类,用于遍历Container
中的元素。内部类Iterator
可以访问外部类Container
的私有成员items
,实现了对容器元素的迭代功能。
总之,内部类是 C++ 中一种强大的特性,可以用于封装、实现设计模式和提高代码的组织性。在使用内部类时,需要注意命名冲突、作用域和生命周期等问题,以确保代码的正确性和可维护性
十一.匿名对象
在 C++ 中,匿名对象是指没有名字的对象。它通常在需要临时对象的情况下使用。
一、作用
- 临时使用:匿名对象主要用于在表达式中提供临时值或执行一次性操作。由于它们没有名字,不能在多个地方被引用,通常在创建后立即使用。
- 避免命名冲突:在某些情况下,使用匿名对象可以避免为临时对象命名而可能导致的命名冲突。特别是在复杂的代码中,使用匿名对象可以使代码更加简洁和清晰。
- 提高代码效率:对于一些不需要长期存在的对象,使用匿名对象可以避免不必要的对象命名和存储,从而提高代码的执行效率。
二、语法特点
- 创建方式:匿名对象通过直接调用构造函数创建,没有对象名。例如:
class MyClass {
public:
MyClass(int value) {
std::cout << "Constructor called with value: " << value << std::endl;
}
};
int main() {
MyClass(10); // 创建匿名对象
return 0;
}
在这个例子中,创建了一个MyClass
类型的匿名对象,传递参数10
给构造函数。
- 生命周期:匿名对象的生命周期通常在创建它的表达式结束时结束。例如,在函数调用中创建的匿名对象,在函数调用结束后被销毁。
三、使用场景
- 函数参数传递:匿名对象可以作为函数的参数传递,特别是当只需要一次性使用对象时。例如:
void printObject(const MyClass& obj) {
std::cout << "Printing object: ";
// 函数体可以使用 obj 进行操作
}
int main() {
printObject(MyClass(20)); // 创建匿名对象并传递给函数
return 0;
}
- 返回值优化:在某些情况下,函数可以返回匿名对象,利用返回值优化(RVO)或命名返回值优化(NRVO)来避免不必要的对象复制。例如:
MyClass createObject() {
return MyClass(30); // 返回匿名对象
}
int main() {
MyClass obj = createObject();
return 0;
}
- 表达式中的临时值:匿名对象可以在表达式中提供临时值,例如在赋值操作、条件判断等中。例如:
int main() {
int result = (MyClass(40).someMemberFunction() + MyClass(50).anotherMemberFunction());
return 0;
}
在这个例子中,创建了两个匿名对象,并调用它们的成员函数,将结果用于表达式中。
总之,匿名对象在 C++ 中是一种方便的方式,可以在需要临时对象的情况下提高代码的简洁性和效率。但需要注意匿名对象的生命周期和使用场景,以确保代码的正确性。
十二.拷贝对象时的一些编译器优化
在 C++ 中,当进行对象拷贝时,编译器可能会进行一些优化以提高性能。以下是一些常见的编译器优化:
一、返回值优化和命名返回值优化
-
作用:
- 当一个函数按值返回一个对象时,编译器可以避免创建临时对象,直接将返回值构造在目标位置。这可以减少对象的构造和析构次数,提高性能。
- RVO 是指在没有命名的情况下,编译器对返回值进行优化。NRVO 则是在有命名的情况下进行的优化。
-
示例:
class MyClass {
public:
MyClass(int value) : data(value) {}
MyClass(const MyClass& other) : data(other.data) {
std::cout << "Copy constructor called." << std::endl;
}
int getData() const { return data; }
private:
int data;
};
MyClass createObject() {
return MyClass(10);
}
int main() {
MyClass obj = createObject();
return 0;
}
在这个例子中,当createObject
函数返回时,编译器可能会进行 RVO 优化,直接在main
函数中的obj
对象的位置构造返回值,而不创建临时对象。
二、拷贝省略(Copy Elision)
-
作用:
- 在某些情况下,编译器可以省略对象的拷贝构造函数的调用,直接将源对象的内容构造到目标对象中。这可以减少不必要的拷贝操作,提高性能。
-
示例:
class MyClass {
public:
MyClass(int value) : data(value) {}
MyClass(const MyClass& other) : data(other.data) {
std::cout << "Copy constructor called." << std::endl;
}
int getData() const { return data; }
private:
int data;
};
void func(MyClass obj) {
// 函数体
}
int main() {
MyClass obj1(10);
func(obj1);
return 0;
}
在这个例子中,当将obj1
作为参数传递给func
函数时,编译器可能会进行拷贝省略优化,直接在func
函数的参数位置构造obj1
的副本,而不调用拷贝构造函数。
三、移动语义(Move Semantics)
-
作用:
- C++11 引入了移动语义,允许在某些情况下将资源从一个对象转移到另一个对象,而不是进行传统的拷贝操作。这对于拥有动态分配资源的对象特别有用,可以避免不必要的资源复制和释放。
-
示例:
class MyClass {
public:
MyClass(int value) : data(new int(value)) {}
~MyClass() { delete data; }
MyClass(const MyClass& other) : data(new int(*other.data)) {
std::cout << "Copy constructor called." << std::endl;
}
MyClass(MyClass&& other) : data(other.data) {
other.data = nullptr;
std::cout << "Move constructor called." << std::endl;
}
int getData() const { return *data; }
private:
int* data;
};
int main() {
MyClass obj1(10);
MyClass obj2 = std::move(obj1);
return 0;
}
在这个例子中,当使用std::move
将obj1
转移给obj2
时,编译器会调用移动构造函数,而不是拷贝构造函数。移动构造函数将资源(这里是动态分配的整数指针)从obj1
转移到obj2
,避免了资源的复制。
总之,编译器在进行对象拷贝时可能会进行多种优化,以提高性能。了解这些优化可以帮助你编写更高效的 C++ 代码。同时,合理使用 C++11 引入的移动语义等特性,可以进一步提高代码的性能和资源管理效率。
十三.理解封装
封装是面向对象编程中的一个重要概念,在 C++ 中有着广泛的应用。
一、定义与作用
封装指的是将数据和操作数据的方法捆绑在一起,隐藏对象的内部实现细节,只对外提供必要的接口。其主要作用有以下几点:
- 信息隐藏:通过将数据成员设为私有(private)或受保护(protected),可以防止外部代码直接访问和修改这些数据,从而保护数据的完整性和一致性。只有通过类提供的公共(public)成员函数才能访问和修改数据,这样可以更好地控制数据的访问权限。
- 模块化设计:封装使得类的内部实现与外部使用分离,提高了代码的模块化程度。不同的类可以独立开发和维护,只要保持公共接口不变,内部实现的修改不会影响到其他部分的代码。
- 代码可维护性:封装使得代码更易于理解和维护。外部代码只需要关注类的公共接口,而不需要了解内部实现细节。当需要修改内部实现时,只需要在类的内部进行修改,不会影响到外部代码的使用。
二、在 C++ 中的实现方式
- 访问修饰符:C++ 中有三种访问修饰符,分别是 public、private 和 protected。
- public:公共成员可以在任何地方被访问,包括类的外部。
- private:私有成员只能在类的内部被访问,类的外部无法直接访问。
- protected:受保护成员可以在类的内部和派生类中被访问,类的外部无法直接访问。
例如:
class MyClass {
private:
int privateData;
protected:
int protectedData;
public:
MyClass(int priv, int prot) : privateData(priv), protectedData(prot) {}
void setPrivateData(int value) {
privateData = value;
}
int getPrivateData() const {
return privateData;
}
void setProtectedData(int value) {
protectedData = value;
}
int getProtectedData() const {
return protectedData;
}
};
在这个例子中,privateData
是私有成员,只能通过类的公共成员函数setPrivateData
和getPrivateData
来访问和修改。protectedData
是受保护成员,可以在派生类中被访问和修改。
- 封装的层次:封装可以在不同的层次上实现。
- 类级封装:将数据和方法封装在类中,通过类的接口来访问和操作数据。
- 模块级封装:将一组相关的类封装在一个模块中,通过模块的接口来访问和操作这些类。
- 系统级封装:将整个系统封装在一个黑盒中,只对外提供必要的接口,隐藏系统的内部实现细节。
三、封装的好处与注意事项
-
好处:
- 提高代码的安全性:通过隐藏内部实现细节,减少了外部代码对数据的错误修改的可能性。
- 增强代码的可复用性:封装好的类可以在不同的项目中重复使用,只需要关注类的公共接口,而不需要了解内部实现细节。
- 便于团队协作:不同的开发人员可以独立开发和维护不同的类,只要保持公共接口不变,内部实现的修改不会影响到其他开发人员的工作。
-
注意事项:
- 不要过度封装:虽然封装可以提高代码的安全性和可维护性,但过度封装可能会导致代码的复杂性增加,降低代码的可读性和可维护性。在封装时,应该根据实际需求合理地选择封装的程度。
- 保持接口的稳定性:一旦类的公共接口确定下来,就应该尽量保持接口的稳定性,避免频繁地修改接口。如果需要修改接口,应该考虑到对现有代码的影响,并提供适当的过渡方案。
总之,封装是面向对象编程中的一个重要概念,它可以提高代码的安全性、可维护性和可复用性。在 C++ 中,通过访问修饰符和合理的设计,可以实现良好的封装效果。