C++(10)之类语法分析(2)
Author: Once Day Date: 2024年8月17日
一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦…
漫漫长路,有人对你微笑过嘛…
全系列文章可参考专栏: 源码分析_Once-Day的博客-CSDN博客
参考文章:
- C++ 重载运算符和重载函数 | 菜鸟教程 (runoob.com)
- 类和结构 (C++) | Microsoft Learn
- C++ 类 & 对象_w3cschool
- 复制构造函数和复制赋值运算符 (C++) | Microsoft Learn
- 如何:定义移动构造函数和移动赋值运算符 (C++) | Microsoft Learn
- 委托构造函数 (C++) | Microsoft Learn
- 指向成员的指针 | Microsoft Learn
- 对象生存期和资源管理 (RAII) | Microsoft Learn
- 友元 (C++) | Microsoft Learn
- 用户定义的类型转换 (C++) | Microsoft Learn
文章目录
- C++(10)之类语法分析(2)
- 1. 类高级特性
- 1.1 运算符重载
- 1.2 友元函数
- 1.3 类型转换
- 1.4 特殊成员函数
- 1.5 禁用方法
- 1.6 返回值类型
- 1.7 内存处理(new/delete)
- 1.8 嵌套结构体
- 1.9 成员初始化列表
1. 类高级特性
基础类的介绍请参考文章:C++(10)类语法分析(1)-CSDN博客
1.1 运算符重载
C++ 允许在同一作用域中的某个函数和运算符指定多个定义,分别称为函数重载和运算符重载。重载声明是指一个与之前已经在该作用域内声明过的函数或方法具有相同名称的声明,但是它们的参数列表和定义(实现)不相同。
当调用一个重载函数或重载运算符时,编译器通过把所使用的参数类型与定义中的参数类型进行比较,决定选用最合适的定义。选择最合适的重载函数或重载运算符的过程,称为重载决策。
运算符重载是C++的一个强大特性,它允许为用户定义的类型自定义运算符的行为。通过运算符重载,我们可以使自定义类型的对象支持各种运算符,如算术运算符、比较运算符、输入输出运算符等,从而提高代码的可读性和表现力。
运算符重载的语法,运算符重载的实现方式是定义一个特殊的成员函数或全局函数,函数名为operator
后跟运算符符号。例如,重载+
运算符的函数名为operator+
。
class MyClass {
public:
MyClass operator+(const MyClass& other); // 成员函数形式的运算符重载
};
MyClass operator+(const MyClass& lhs, const MyClass& rhs); // 全局函数形式的运算符重载
重载算术运算符,可以重载各种算术运算符,如+
、-
、*
、/
等,以支持自定义类型的算术运算。
class Complex {
public:
Complex(double real, double imag) : real(real), imag(imag) {}
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}
private:
double real;
double imag;
};
下面是可重载的运算符列表:
双目算术运算符 | + (加),-(减),*(乘),/(除),% (取模) |
---|---|
关系运算符 | ==(等于),!= (不等于),< (小于),> (大于),<=(小于等于),>=(大于等于) |
逻辑运算符 | ||(逻辑或),&&(逻辑与),!(逻辑非) |
单目运算符 | + (正),-(负),*(指针),&(取地址) |
自增自减运算符 | ++(自增),–(自减) |
位运算符 | | (按位或),& (按位与),~(按位取反),^(按位异或),,<< (左移),>>(右移) |
赋值运算符 | =, +=, -=, *=, /= , % = , &=, |=, ^=, <<=, >>= |
空间申请与释放 | new, delete, new[ ] , delete[] |
其他运算符 | ()(函数调用),->(成员访问),,(逗号),[](下标) |
下面是不可重载的运算符列表:
-
.
,成员访问运算符。 -
.*
,->\*
,成员指针访问运算符。 -
::
,域运算符。 -
sizeof
,长度运算符。 -
?:
,条件运算符。 -
#
,预处理符号。
重载运算符还需要满足一下限制:
- 重载运算符必须至少有一个操作数是用户定义的类型。这意味着不能重载两个内置类型的运算符。
- 重载运算符的优先级和结合性是固定的,不能被改变。重载运算符的优先级和结合性与对应的内置运算符相同。
- 重载运算符必须是公有的(public)成员函数或全局函数。如果是成员函数,则其中一个操作数必须是该类的对象。
- 重载运算符时不能改变操作数的个数。例如,二元运算符重载后仍然是二元的,一元运算符重载后仍然是一元的。
- 重载运算符时不能改变运算符的语法含义。例如,不能将加法运算符+重载为执行减法操作。
- 重载运算符时不能创建新的运算符,只能重载已有的运算符。
- 某些运算符必须以成员函数的形式进行重载,如赋值运算符=、下标运算符[]、函数调用运算符()和成员访问箭头运算符->。
- 重载运算符时,参数列表中至少应有一个参数是类的对象或对象的引用。
- 静态成员函数不能用于重载运算符。
1.2 友元函数
在C++中,友元函数(Friend Function)是一种特殊的函数,它可以访问类的私有成员和保护成员,即使它不是类的成员函数。这打破了类的封装性,但在某些情况下,友元函数可以提供更加灵活和方便的方式来操作类的私有数据。
要将一个函数声明为类的友元函数,需要在类的定义中使用关键字 friend
,并在函数声明前加上 friend
关键字。友元函数可以是全局函数,也可以是另一个类的成员函数。
class MyClass {
private:
int secret;
public:
MyClass(int s) : secret(s) {}
friend void friendFunction(MyClass& obj);
};
void friendFunction(MyClass& obj) {
// 友元函数可以访问 MyClass 的私有成员
std::cout << "My friend knows my secret: " << obj.secret << std::endl;
}
在上面的例子中,friendFunction
被声明为 MyClass
的友元函数。尽管 friendFunction
不是 MyClass
的成员函数,但它可以访问 MyClass
的私有成员 secret
。
当两个类需要频繁地访问对方的私有成员时,使用友元函数可以避免繁琐的公有接口设计。
1.3 类型转换
在C++中,类可以定义自动类型转换和强制类型转换,以实现类对象与其他类型之间的转换。
自动类型转换(Implicit Type Conversion):有一个名为 Fruit
的类,它表示一种水果。现在,把这个水果变成一个整数,代表这个水果的数量。可以通过定义一个接受 Fruit
对象的构造函数来实现这个功能。
class Fruit {
public:
Fruit(int quantity) : quantity_(quantity) {}
// 定义转换函数,将 Fruit 对象转换为整数
operator int() const {
return quantity_;
}
private:
int quantity_;
};
// 使用示例
Fruit apple(5);
int quantity = apple; // 自动将 Fruit 对象转换为整数
在这个例子中,定义了一个转换函数 operator int()
,它允许 Fruit
对象自动转换为整数。将 Fruit
对象赋值给一个整数变量时,编译器会自动调用这个转换函数,将 Fruit
对象转换为对应的整数值。
隐式类型转换是自动进行的,不需要显式地调用转换函数。以下是一些常见的隐式类型转换场景:
- 将类对象赋值给其他类型的变量。
- 将类对象作为函数参数传递。
- 在条件语句中使用类对象。
- 返回值声明为类对象。
此外,这里定义了一个转换函数operator int() const
,其格式通常如下:
operator typename();
转换函数必须是类方法,且不能指定返回类型,不能有参数,一般最好进行显示声明(explicit),可以避免隐式自动转换。
强制类型转换(Explicit Type Conversion):现在,让我们考虑一个更复杂的情况。假设有一个名为 Juice
的类,它表示一种果汁。想把整数转换为 Juice
对象,代表制作果汁所需的水果数量。但是这个转换不希望是自动进行的,而是要求显式地进行转换。
class Juice {
public:
explicit Juice(int quantity) : quantity_(quantity) {}
private:
int quantity_;
};
// 使用示例
Juice orangeJuice = Juice(10); // 正确,显式地创建 Juice 对象
Juice appleJuice = 5; // 错误,不允许自动转换
在这个例子中,使用 explicit
关键字来修饰 Juice
类的构造函数,表示不允许进行隐式转换。这意味着不能直接将整数赋值给 Juice
对象,而必须显式地调用构造函数来创建 Juice
对象。
通过使用 explicit
关键字,可以防止意外的自动转换,提高代码的可读性和安全性。
自动类型转换允许类对象与其他类型之间的隐式转换,通过定义转换函数来实现。而强制类型转换要求显式地进行转换,通过使用 explicit
关键字来禁止自动转换。
当提供两个或多个用户定义的用于执行相同转换的转换时,该转换将被视为不明确。 这种不明确性是一个错误,因为编译器无法确定应选择哪一个可用转换。
1.4 特殊成员函数
在C++类中,有一些特殊的成员函数,它们在类的生命周期中扮演着重要的角色。
-
默认构造函数(Default Constructor),默认构造函数是一种特殊的构造函数,它不接受任何参数,或者接受的参数都有默认值。当创建一个类的对象时,如果没有显式地调用其他构造函数,默认构造函数会被自动调用。默认构造函数的作用是初始化对象的成员变量,将其设置为合适的默认值。
class_name::class_name() {}
-
默认析构函数(Default Destructor),默认析构函数是一种特殊的成员函数,它在对象的生命周期结束时被自动调用。析构函数的作用是释放对象所占用的资源,如动态分配的内存、打开的文件等。默认析构函数不接受任何参数,也没有返回值。
class_name::~class_name() {}
-
复制构造函数(Copy Constructor),复制构造函数是一种特殊的构造函数,它接受一个同类型对象的常量引用作为参数。复制构造函数的作用是根据已有对象创建一个新对象,并将已有对象的成员变量的值复制到新对象中。
class_name(const class_name &);
当以下情况发生时,复制构造函数会被调用:
- 用一个对象初始化另一个同类型的对象。
- 函数按值传递对象。
- 函数返回一个对象。
由于按值传递对象将调用复制构造函数,因此应该按照引用传递对象,可以节省调用构造函数的时间和存储新对象的空间。
复制构造函数只是浅复制,即简单的复制非静态成员的值,所以指针指向的区域还是共用的,这种情况下需要定义一个复制构造函数,进行深度赋值操作。
-
赋值运算符重载(Assignment Operator),赋值运算符重载是一种特殊的成员函数,它定义了对象之间的赋值操作。默认情况下,赋值运算符执行逐个成员的赋值操作。但是,有时需要自定义赋值运算符的行为,以处理特殊的赋值逻辑,如深拷贝、资源管理等。
class_name & class_name::operator=(const class_name &);
由于目标对象可能引用了以前分配的数据,所以函数应该使用delete[]来释放这些数据,同时应该避免将对象赋给自己。
-
移动构造函数(Move Constructor),移动构造函数是C++11引入的新特性,它接受一个同类型对象的右值引用作为参数。移动构造函数的作用是将资源从一个对象转移到另一个对象,而不是进行复制。这可以提高性能,避免不必要的复制操作。
class_name::class_name(class_name &&);
移动构造函数需要对右值才能生效(没有实际存储空间的值),如下所示:
class_name two = one; // 匹配中复制构造函数 class_name four (one + three); // 匹配中移动构造函数
-
移动赋值运算符重载(Move Assignment Operator),移动赋值运算符重载也是C++11引入的新特性,它允许将一个对象的资源转移到另一个对象,而不是进行复制。与移动构造函数类似,移动赋值运算符重载接受一个同类型对象的右值引用作为参数,并将资源从源对象转移到目标对象。
class_name & class_name::operator=(class_name &&);
如果要对左值强行使用移动赋值语义,可以通过static_cast强制类型转换为
class_name &&
,或者使用utility
中的std::move
函数。
委托构造函数(Delegating Constructors),委托构造函数允许一个构造函数调用同一个类中的另一个构造函数。这样可以避免在多个构造函数中重复相同的初始化代码。
class Person {
public:
Person(const std::string& name, int age) : name(name), age(age) {
// ...
}
Person(const std::string& name) : Person(name, 0) {
// ...
}
// ...
private:
std::string name;
int age;
};
在上述示例中,第二个构造函数委托给了第一个构造函数,并将年龄参数设置为默认值0。
继承构造函数(Inheriting Constructors):继承构造函数允许派生类直接使用基类的构造函数,而无需在派生类中显式定义对应的构造函数。这可以简化派生类的构造函数编写。
class Base {
public:
Base(int x) : x(x) {
// ...
}
// ...
protected:
int x;
};
class Derived : public Base {
public:
using Base::Base;
// ...
};
在上述示例中,派生类 Derived
使用 using Base::Base;
语句继承了基类 Base
的构造函数。这样,Derived
类可以直接使用 Base
类的构造函数,而无需显式定义自己的构造函数。
1.5 禁用方法
默认方法(Default Functions),默认方法是指编译器自动生成的特殊成员函数,包括默认构造函数、析构函数、复制构造函数和赋值运算符重载函数。在C++11之前,如果没有显式定义这些函数,编译器会自动生成它们的默认实现。但是,从C++11开始,可以使用 =default
语法来显式地指定使用编译器生成的默认实现。
class MyClass {
public:
MyClass() = default;
~MyClass() = default;
MyClass(const MyClass&) = default;
MyClass& operator=(const MyClass&) = default;
// ...
};
使用 =default
可以明确表示使用编译器生成的默认实现,提高代码的可读性和维护性。
禁用方法(Deleted Functions),禁用方法是指使用 =delete
语法来明确禁止使用某个函数。当我们不希望某个函数被调用时,可以将其声明为禁用方法。这样,如果在代码中尝试调用被禁用的函数,编译器会报错。
class NonCopyable {
public:
NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
// ...
};
在上述示例中,通过将复制构造函数和赋值运算符重载函数声明为禁用方法,我们可以禁止对象的复制和赋值操作。
1.6 返回值类型
在C++类中,函数的返回值类型可以是值、常量对象、引用或常量引用。不同的返回值类型在不同的场景下有其特定的用途。
(1) 值返回(Return by Value):当函数返回一个值时,它会创建一个临时对象,并将函数内部对象的值复制到该临时对象中。这个临时对象将作为函数的返回值被返回给调用方。值返回适用于以下情况:
- 返回的对象较小,复制成本较低。
- 返回的对象是函数内部创建的,没有与外部共享状态。
- 不需要在函数外部修改返回的对象。
class Point {
public:
int getX() const { return x; }
// ...
private:
int x;
// ...
};
(2) 常量对象返回(Return by Const Object),当函数返回一个常量对象时,它表示返回的对象不能被修改。这提供了一种安全性,确保调用方不会意外地修改返回的对象。常量对象返回适用于以下情况:
- 返回的对象不应被修改。
- 希望强调返回对象的不可变性。
class Date {
public:
const std::string& getFormattedDate() const {
// ...
return formattedDate;
}
// ...
private:
std::string formattedDate;
// ...
};
(3) 引用返回(Return by Reference):当函数返回一个引用时,它返回的是对象的引用,而不是对象的副本。引用返回允许在函数外部直接访问和修改函数内部的对象。引用返回适用于以下情况:
- 返回的对象是函数外部已经存在的对象。
- 希望在函数外部能够修改返回的对象。
- 返回的对象较大,复制成本较高,希望避免不必要的复制。
class Array {
public:
int& operator[](size_t index) {
return data[index];
}
// ...
private:
int data[100];
// ...
};
(4) 常量引用返回(Return by Const Reference):当函数返回一个常量引用时,它返回的是对象的引用,但该引用不允许修改对象。常量引用返回提供了访问对象的能力,同时确保了对象的不可变性。常量引用返回适用于以下情况:
- 返回的对象是函数外部已经存在的对象。
- 希望在函数外部能够访问返回的对象,但不允许修改它。
- 返回的对象较大,复制成本较高,希望避免不必要的复制。
class Person {
public:
const std::string& getName() const {
return name;
}
// ...
private:
std::string name;
// ...
};
1.7 内存处理(new/delete)
在C++中,使用new运算符动态分配对象时,会先分配内存,然后调用类的构造函数来初始化对象。相应地,使用delete运算符释放对象时,会先调用类的析构函数来清理资源,然后再释放内存。
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;
}
定位new运算符(Placement New Operator),定位new运算符允许在已分配的内存上构造对象。它接受一个指向内存位置的指针作为参数,并在该位置构造对象。定位new运算符通常用于在特定的内存位置上构造对象,如在预分配的内存池中创建对象。
int main() {
char* buffer = new char[sizeof(MyClass)]; // 分配原始内存
MyClass* obj = new (buffer) MyClass(42); // 使用定位new在buffer上构造对象
obj->~MyClass(); // 显式调用析构函数
delete[] buffer; // 释放原始内存
return 0;
}
在某些情况下,我们可能需要显式调用对象的析构函数,而不是依赖于delete运算符。这通常发生在使用定位new运算符构造对象时,或者在使用placement new构造对象数组时。
int main() {
char* buffer = new char[sizeof(MyClass) * 3]; // 分配原始内存
MyClass* obj1 = new (buffer) MyClass(); // 使用定位new构造对象
MyClass* obj2 = new (buffer + sizeof(MyClass)) MyClass();
MyClass* obj3 = new (buffer + sizeof(MyClass) * 2) MyClass();
obj3->~MyClass(); // 显式调用析构函数
obj2->~MyClass();
obj1->~MyClass();
delete[] buffer; // 释放原始内存
return 0;
}
在上述示例中,我们使用定位new在预分配的内存上构造了多个对象。由于没有使用delete运算符,我们需要显式调用每个对象的析构函数来进行清理。注意,析构函数的调用顺序与构造顺序相反。
1.8 嵌套结构体
在C++中,可以在一个类或结构体内部定义另一个类或结构体,这种内部定义的类或结构体称为嵌套类(Nested Class)或嵌套结构体(Nested Struct)。嵌套类和嵌套结构体提供了一种将相关类或结构体组织在一起的方式,增强了代码的可读性和封装性。
嵌套类(Nested Class),嵌套类是在另一个类的内部定义的类。它可以访问外部类的成员,包括私有成员和保护成员。嵌套类通常用于实现与外部类密切相关的功能,或者用于隐藏实现细节。
class Outer {
public:
void outerMethod() {
Inner inner;
inner.innerMethod();
}
private:
class Inner {
public:
void innerMethod() {
std::cout << "Inner method called" << std::endl;
}
};
};
在上述示例中,Inner
类是在Outer
类内部定义的嵌套类。Outer
类的成员函数outerMethod()
可以直接创建和使用Inner
类的对象。
嵌套结构体(Nested Struct),嵌套结构体与嵌套类类似,只是使用struct
关键字定义。嵌套结构体的成员默认为公有访问权限,而嵌套类的成员默认为私有访问权限。
struct Outer {
void outerMethod() {
Inner inner;
inner.x = 10;
}
struct Inner {
int x;
};
};
在上述示例中,Inner
结构体是在Outer
结构体内部定义的嵌套结构体。Outer
结构体的成员函数outerMethod()
可以直接访问和修改Inner
结构体的成员变量x
。
要在外部类或结构体之外访问嵌套类或嵌套结构体,需要使用外部类或结构体的作用域解析运算符::
。
class Outer {
public:
class Inner {
public:
void innerMethod() {
std::cout << "Inner method called" << std::endl;
}
};
};
int main() {
Outer::Inner inner;
inner.innerMethod();
return 0;
}
在上述示例中,我们在main()
函数中创建了Outer::Inner
类型的对象,并调用了其成员函数innerMethod()
。
1.9 成员初始化列表
在C++中,成员初始化列表(Member Initialization List)是一种在构造函数中初始化类成员变量的方式。它提供了一种更高效、更清晰的方法来初始化成员变量,特别是对于常量成员、引用成员以及没有默认构造函数的类类型成员。
常见使用场景如下:
- 常量成员变量:常量成员变量必须在构造函数的成员初始化列表中初始化,因为它们不能在构造函数体内被赋值。
- 引用成员变量:引用成员变量必须在构造函数的成员初始化列表中初始化,因为引用必须在定义时绑定到一个对象。
- 没有默认构造函数的类类型成员:如果类成员变量是没有默认构造函数的类类型,那么必须在成员初始化列表中显式初始化它们。
- 基类构造函数初始化:在派生类的构造函数中,必须使用成员初始化列表来调用基类的构造函数。
- 提高效率:对于内置类型和有默认构造函数的类类型成员,使用成员初始化列表可以避免默认构造和再赋值的过程,提高效率。
使用限制如下:
- 成员初始化列表中的成员变量初始化顺序与它们在类中声明的顺序相同,而不是按照成员初始化列表中的顺序。
- 成员初始化列表不能用于初始化静态成员变量,静态成员变量需要在类外部单独初始化。
- 如果一个成员变量在成员初始化列表中被初始化,就不能再在构造函数体内对其赋值,否则会报错。
class MyClass {
public:
MyClass(int a, const std::string& s) : constantMember(42), referenceMember(s), intMember(a),
objectMember(a, s) {
// 构造函数体
}
private:
const int constantMember;
const std::string& referenceMember;
int intMember;
SomeClass objectMember;
};
在上述示例中,MyClass
的构造函数使用成员初始化列表来初始化常量成员constantMember
、引用成员referenceMember
、内置类型成员intMember
以及类类型成员objectMember
。成员初始化列表中的初始化顺序与成员变量在类中声明的顺序相同。
C++11引入了类内初始化(In-class Initialization)特性,允许在类内部直接为非静态成员变量提供默认初始值。这样可以简化构造函数的编写,同时提供了一种更清晰、更一致的方式来初始化成员变量。
class MyClass {
public:
MyClass(int a) : intMember(a) {
// 构造函数体
}
private:
int intMember = 0;
std::string stringMember = "Default";
std::vector<int> vectorMember{1, 2, 3};
};
在上述示例中,intMember
、stringMember
和vectorMember
都在类内部提供了默认初始值。如果构造函数没有显式初始化这些成员变量,它们将使用类内部指定的默认值。
需要注意的是,类内初始化不能用于常量成员、引用成员以及没有默认构造函数的类类型成员,这些成员变量仍然需要在成员初始化列表中显式初始化。
Once Day
也信美人终作土,不堪幽梦太匆匆......
如果这篇文章为您带来了帮助或启发,不妨点个赞👍和关注,再加上一个小小的收藏⭐!
(。◕‿◕。)感谢您的阅读与支持~~~