c++高频总结九
友元friend介绍
在 C++ 中,友元(friend) 是一种特殊机制,用于允许一个类或函数访问另一个类的私有或受保护成员。通常情况下,类的私有(private)和受保护(protected)成员只能被该类的成员函数或其派生类访问,而友元提供了一种例外。
友元的作用
- 提高类之间的紧密协作性。
- 在保持封装的同时,实现更灵活的访问控制。
- 用于操作复杂类的内部数据而不暴露实现细节。
友元的类型
1. 友元函数
一个普通的函数可以被声明为某个类的友元,从而可以直接访问该类的私有和受保护成员。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;
class MyClass {
private:
int value;
public:
MyClass(int val) : value(val) {}
// 声明友元函数
friend void printValue(const MyClass& obj);
};
// 友元函数定义
void printValue(const MyClass& obj) {
cout << "Value: " << obj.value << endl;
}
int main() {
MyClass obj(42);
printValue(obj); // 访问私有成员
return 0;
}
2. 友元类
一个类可以被声明为另一个类的友元,从而可以访问该类的私有和受保护成员。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
using namespace std;
class MyClass {
private:
int value;
public:
MyClass(int val) : value(val) {}
// 声明 FriendClass 为友元类
friend class FriendClass;
};
class FriendClass {
public:
void display(const MyClass& obj) {
cout << "Value: " << obj.value << endl; // 直接访问私有成员
}
};
int main() {
MyClass obj(100);
FriendClass f;
f.display(obj);
return 0;
}
3. 友元成员函数
一个类的特定成员函数可以被声明为另一个类的友元,而不是整个类。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
using namespace std;
class MyClass; // 前向声明
class AnotherClass {
public:
void accessMyClass(const MyClass& obj);
};
class MyClass {
private:
int value;
public:
MyClass(int val) : value(val) {}
// 声明 AnotherClass 的成员函数为友元
friend void AnotherClass::accessMyClass(const MyClass& obj);
};
void AnotherClass::accessMyClass(const MyClass& obj) {
cout << "Value: " << obj.value << endl;
}
int main() {
MyClass obj(25);
AnotherClass anotherObj;
anotherObj.accessMyClass(obj);
return 0;
}
友元的优缺点
优点:
- 灵活性:友元允许对类的内部细节进行访问,适合实现复杂关系。
- 效率:直接访问成员变量,省去冗余的 getter 和 setter 函数。
缺点:
- 破坏封装:友元会增加耦合性,使类变得不那么独立。
- 维护复杂:友元关系增加了代码维护的复杂性。
友元的注意事项
- 友元关系是单向的,类 A 的友元类 B 并不意味着 A 是 B 的友元。
- 友元关系不是继承的,派生类不会继承基类的友元。
- 友元函数和友元类定义在类的任何访问权限下都可以。
通过友元机制,可以平衡封装性与功能需求,但应谨慎使用以避免代码复杂度和耦合度的增加。
move函数
在 C++ 中,std::move
是一个标准库函数,用于实现 移动语义。它位于 <utility>
头文件中。std::move
并不是一个实际移动操作的函数,而是一个类型转换工具,它将一个对象显式地转换为一个右值引用(rvalue reference
),从而可以将资源从一个对象转移到另一个对象,而无需昂贵的拷贝操作。
使用场景
- 避免拷贝:将资源从一个对象“移动”到另一个对象,而不是深拷贝数据。
- 优化性能:当对象内部包含动态资源(如堆内存、文件句柄等),移动操作可以通过资源转移提高效率。
移动语义和 std::move
的核心概念
1. 移动构造函数
移动构造函数接受一个右值引用作为参数,通常用于“接管”另一个对象的资源,并将其内部资源置为空或默认状态。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
#include <vector>
using namespace std;
class MyClass {
private:
int* data; // 动态分配的资源
size_t size;
public:
// 构造函数
MyClass(size_t s) : size(s), data(new int[s]) {
cout << "Constructing object with size: " << size << endl;
}
// 移动构造函数
MyClass(MyClass&& other) noexcept : size(other.size), data(other.data) {
other.data = nullptr; // 释放源对象的指针
other.size = 0;
cout << "Moving object of size: " << size << endl;
}
// 析构函数
~MyClass() {
if (data) {
cout << "Destroying object with size: " << size << endl;
delete[] data;
}
}
};
int main() {
MyClass obj1(10); // 调用构造函数
MyClass obj2 = std::move(obj1); // 调用移动构造函数
return 0;
}
输出示例:
1
2
3
Constructing object with size: 10
Moving object of size: 10
Destroying object with size: 0
2. 移动赋值运算符
移动赋值运算符用于将一个右值对象的资源转移到当前对象。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <string>
using namespace std;
class MyClass {
private:
string data;
public:
MyClass(string str) : data(move(str)) {}
// 移动赋值运算符
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
data = move(other.data); // 转移资源
}
cout << "Move assignment: " << data << endl;
return *this;
}
};
int main() {
MyClass obj1("Hello");
MyClass obj2("World");
obj2 = std::move(obj1); // 调用移动赋值运算符
return 0;
}
std::move
的作用
- 将左值(
lvalue
)转换为右值引用(rvalue reference
)。 - 注意:
std::move
并不会真正移动数据,它只是显式地告诉编译器可以“转移”资源。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v1 = {1, 2, 3};
vector<int> v2 = std::move(v1); // 将 v1 的资源转移到 v2
cout << "v1 size: " << v1.size() << endl; // v1 的数据已被转移,通常为空
cout << "v2 size: " << v2.size() << endl;
return 0;
}
输出示例:
1
2
v1 size: 0
v2 size: 3
std::move
使用注意事项
- 只用于临时转移资源: 一旦调用
std::move
,原对象的状态通常会被设置为未定义或清空。不要再依赖原对象的值。 - 避免误用: 不要将
std::move
用在需要保留原对象状态的场景,否则可能引发错误。 - 与移动语义配合:
std::move
通常与用户定义的移动构造函数和移动赋值运算符配合使用。
std::move
的本质
1
2
3
4
template<typename T>
typename remove_reference<T>::type&& move(T&& arg) noexcept {
return static_cast<typename remove_reference<T>::type&&>(arg);
}
std::move
将对象显式转换为右值引用,使其可以触发移动语义。
总结
std::move
是移动语义的核心工具,用于性能优化。- 它的功能是将一个左值转换为右值引用,从而允许资源的高效转移。
- 使用时需要注意资源管理,确保原对象在转移后仍然处于可预期状态。
模板类的作用
模板类 是 C++ 中的一种泛型编程工具,用于创建可以适应不同数据类型的类。它通过在类定义中引入类型参数,使得一个类可以操作多种类型,而无需为每种类型都定义一个单独的类。
模板类的作用
- 泛型编程:支持多种数据类型,避免代码重复。
- 代码复用:实现同一功能的类可以适配不同的数据类型,减少冗余代码。
- 类型安全:模板通过类型检查提供了更安全的代码结构。
- 灵活性:支持自定义的任意类型(用户定义类型、内置类型等)。
- 性能优化:与运行时的多态相比,模板通过编译时生成特定类型的代码,通常效率更高。
模板类的语法
模板类定义时使用关键字 template
声明类型参数。常见的类型参数包括:
- T:常用作类型参数的占位符。
- typename 和 class:用来声明模板参数,功能相同(无语义区别)。
模板类基本定义:
1
2
3
4
5
6
7
8
9
10
template <typename T>
class MyClass {
private:
T data;
public:
MyClass(T val) : data(val) {} // 构造函数
void display() const {
std::cout << "Data: " << data << std::endl;
}
};
模板类的使用
示例 1:简单模板类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;
template <typename T>
class MyClass {
private:
T data;
public:
MyClass(T val) : data(val) {} // 构造函数
void display() const {
cout << "Data: " << data << endl;
}
};
int main() {
MyClass<int> obj1(10); // 使用 int 类型
MyClass<double> obj2(5.5); // 使用 double 类型
obj1.display();
obj2.display();
return 0;
}
输出:
1
2
Data: 10
Data: 5.5
示例 2:多类型模板类 模板类可以接受多个类型参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;
template <typename T1, typename T2>
class MyPair {
private:
T1 first;
T2 second;
public:
MyPair(T1 f, T2 s) : first(f), second(s) {}
void display() const {
cout << "First: " << first << ", Second: " << second << endl;
}
};
int main() {
MyPair<int, string> pair1(1, "One");
MyPair<double, char> pair2(3.14, 'A');
pair1.display();
pair2.display();
return 0;
}
输出:
1
2
First: 1, Second: One
First: 3.14, Second: A
模板类的作用详解
1. 解决代码重复问题
如果没有模板类,为不同类型编写类会导致代码重复。
非模板的代码:
1
2
3
4
5
6
7
8
9
class IntClass {
int data;
// 方法省略...
};
class DoubleClass {
double data;
// 方法省略...
};
模板类的代码:
1
2
3
4
5
template <typename T>
class MyClass {
T data;
// 复用逻辑...
};
2. 灵活性和扩展性
模板类可以适配任意类型,甚至是用户自定义的类型。
示例:支持用户自定义类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <string>
using namespace std;
class CustomType {
private:
string name;
public:
CustomType(string n) : name(n) {}
friend ostream& operator<<(ostream& os, const CustomType& obj) {
return os << obj.name;
}
};
template <typename T>
class MyClass {
private:
T data;
public:
MyClass(T val) : data(val) {}
void display() const {
cout << "Data: " << data << endl;
}
};
int main() {
MyClass<int> obj1(42);
MyClass<CustomType> obj2(CustomType("MyCustomType"));
obj1.display();
obj2.display();
return 0;
}
模板类与普通类的区别
特性 | 普通类 | 模板类 |
---|---|---|
类型适配 | 固定某一特定类型 | 支持多种类型 |
代码复用性 | 需要为每种类型重新编写类代码 | 一套代码适配所有类型 |
性能 | 固定编译代码 | 编译时生成特定类型的代码,无额外开销 |
使用复杂性 | 直接定义和使用 | 需要显式指定类型参数 |
模板类的注意事项
- 编译时生成代码:每使用一种类型实例化模板时,编译器会生成一套对应的代码,因此滥用模板可能导致代码膨胀(编译时间和可执行文件大小增加)。
- 不能分离声明与实现(除非使用
template
关键字):模板的实现一般写在头文件中,因为编译器需要在实例化时看到所有模板定义。 - 类型限制:模板类默认支持所有类型,但如果某些操作不适用于某种类型,可能会引发编译错误。
- 特殊化和偏特化:可以为特定类型提供特殊实现。
总结
模板类是 C++ 泛型编程的重要工具,能够显著提高代码的复用性和扩展性,同时通过编译时类型检查保证安全性。它的广泛应用使得 C++ 在处理多种数据类型和复杂场景时更具优势。
模板和泛型的区别
模板(Template) 和 泛型(Generic Programming) 是 C++ 中紧密相关但不同的概念。模板是实现泛型编程的一种工具,而泛型是一种编程范式。下面从多个角度探讨它们的区别。
1. 定义
模板 | 泛型 |
---|---|
模板是 C++ 提供的技术,用于在代码中实现类型参数化。 | 泛型是一种编程思想,用于设计可以适配多种类型的代码。 |
主要指具体的语法和机制(如 template 关键字)。 | 是更高层次的抽象概念,模板只是实现它的一种方式。 |
2. 目的
模板 | 泛型 |
---|---|
通过代码重用,支持为不同类型生成通用代码。 | 设计与类型无关的算法和数据结构,强调抽象和适配性。 |
强调语法和实现细节。 | 强调思想和设计目标。 |
3. 表现形式
模板是 C++ 提供的技术,具体体现在以下形式:
- 函数模板:对不同类型使用相同函数逻辑。
- 类模板:对不同类型使用相同类结构。
泛型则是更抽象的概念,除了 C++ 的模板,其他语言也有自己的泛型编程实现,例如:
- C++:模板(Templates)
- Java/C#:泛型(Generics)
- Rust:Trait Bounds
- Go:泛型接口
4. 实现机制
模板 | 泛型 |
---|---|
模板在 编译时实例化,根据实际类型生成代码(模板实例化)。 | 泛型可以在 编译时 或 运行时 进行类型适配,视语言而定。 |
生成的代码与具体类型紧密绑定,属于 编译时多态。 | 可能采用不同技术,如运行时类型擦除(Java)或编译时检查(C++)。 |
5. 编译期与运行期
- 模板 是编译期的工具。C++ 的模板在编译时展开,针对每种类型生成独立的代码,因此具有高效的运行性能,但可能导致编译时间变长或代码膨胀(code bloat)。
- 泛型 是更广义的概念,某些语言(如 Java)通过 类型擦除 在运行时支持泛型,但性能可能会有所下降。
示例:C++ 模板和 Java 泛型的实现对比
C++ 模板:编译时类型实例化
1
2
3
4
5
6
7
8
9
10
template <typename T>
T add(T a, T b) {
return a + b;
}
int main() {
int x = add<int>(1, 2); // 实例化为 int 类型的代码
double y = add<double>(1.1, 2.2); // 实例化为 double 类型的代码
return 0;
}
Java 泛型:运行时类型擦除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class GenericClass<T> {
private T data;
public GenericClass(T data) {
this.data = data;
}
public T getData() {
return data;
}
}
public class Main {
public static void main(String[] args) {
GenericClass<Integer> obj = new GenericClass<>(10); // 编译时检查类型
System.out.println(obj.getData()); // 运行时处理,但类型擦除机制会将 T 替换为 Object
}
}
6. 灵活性
- 模板 是 C++ 的核心功能,结合模板特化(specialization)和偏特化(partial specialization),可以针对特定类型提供优化代码。
- 泛型 是一种普适性更强的概念,不局限于某种语言,但其具体实现可能无法像 C++ 模板那样支持偏特化等高级功能。
7. 特性对比
特性 | 模板(C++) | 泛型(Java/C# 等) |
---|---|---|
实现方式 | 编译时实例化 | 运行时类型擦除或编译时检查 |
类型安全 | 编译时严格检查 | 编译时检查,但运行时可能涉及类型转换 |
性能 | 高效,无运行时开销 | 类型擦除可能带来运行时开销 |
支持的功能 | 偏特化、特化等高级功能 | 不支持偏特化 |
通用性 | 仅适用于 C++ | 通用于多种编程语言 |
8. 优势和劣势
模板的优势
- 编译期生成代码,无运行时开销。
- 支持高级特性,如偏特化、完全特化。
- 提供更强的灵活性,可以处理复杂的类型操作。
模板的劣势
- 编译时间长,可能导致代码膨胀。
- 错误信息复杂,模板调试难度大。
泛型的优势
- 设计思想更加通用,适用于多种语言。
- 在运行时适配类型,提升代码的适用性。
泛型的劣势
- 某些语言的泛型实现可能影响性能(如 Java 的类型擦除)。
- 对特定类型的优化能力较弱。
总结
- 模板 是 C++ 中用于实现泛型编程的一种具体技术,通过编译时实例化实现高效代码。
- 泛型 是一种更广义的编程范式,不局限于某种语言,用于设计和实现适配多种类型的算法和数据结构。
- 两者关系:模板是实现泛型编程的一种技术手段,而泛型是一种编程思想。
内存管理:C++的new和malloc的区别
在 C++ 中,new
和 malloc
都可以用于动态分配内存,但它们的用途、功能和行为有显著区别。以下是详细对比:
1. 主要区别
特性 | new | malloc |
---|---|---|
语言支持 | 是 C++ 的关键字,仅在 C++ 中使用。 | 是 C 标准库函数,C 和 C++ 都可以使用。 |
分配内存的类型 | 返回一个指定类型的指针,分配的内存是已初始化的。 | 返回 void* ,需手动转换为所需的指针类型。 |
是否调用构造函数 | 调用对象的构造函数(如有),并初始化对象。 | 不会调用构造函数,仅分配内存。 |
内存释放 | 使用 delete 释放内存,调用析构函数(如有)。 | 使用 free 释放内存,不调用析构函数。 |
语法 | 是 C++ 的操作符,不需要引入头文件。 | 是 C 函数,需包含 <cstdlib> 。 |
错误处理 | 分配失败会抛出 std::bad_alloc 异常(可以捕获)。 | 分配失败返回 nullptr ,需要手动检查。 |
2. 使用示例
new
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass() { cout << "Constructor called" << endl; }
~MyClass() { cout << "Destructor called" << endl; }
};
int main() {
// 分配单个对象
MyClass* obj = new MyClass();
// 使用对象
delete obj; // 调用析构函数并释放内存
// 分配数组
MyClass* objArray = new MyClass[3];
delete[] objArray; // 调用析构函数并释放数组内存
return 0;
}
输出示例:
1
2
3
4
5
6
7
8
Constructor called
Destructor called
Constructor called
Constructor called
Constructor called
Destructor called
Destructor called
Destructor called
malloc
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <cstdlib> // 包含 malloc 和 free
using namespace std;
struct MyStruct {
int x;
};
int main() {
// 分配内存
MyStruct* obj = (MyStruct*)malloc(sizeof(MyStruct)); // 必须手动转换类型
if (!obj) {
cout << "Memory allocation failed" << endl;
return 1;
}
obj->x = 10; // 直接使用分配的内存
cout << "x: " << obj->x << endl;
// 释放内存
free(obj);
return 0;
}
输出示例:
1
x: 10
3. 内存分配和初始化
new
:分配内存并初始化
new
会自动计算分配所需的内存大小,并调用构造函数初始化对象。- 如果是基本数据类型,
new
分配的内存会被零初始化或使用用户提供的值。
1
int* p = new int(42); // 分配一个 int,值为 42
malloc
:仅分配内存
malloc
只分配原始内存块,不会初始化。- 需要手动初始化内存。
1
2
int* p = (int*)malloc(sizeof(int));
*p = 42; // 必须手动赋值
4. 内存释放
new/delete
- 使用
delete
释放通过new
分配的内存。 - 如果分配了数组,则需要使用
delete[]
。
1
2
3
4
5
int* p = new int;
delete p;
int* arr = new int[5];
delete[] arr;
malloc/free
- 使用
free
释放通过malloc
分配的内存。 - 没有类似
delete[]
的语法,释放数组与普通内存相同。
1
2
3
4
5
int* p = (int*)malloc(sizeof(int));
free(p);
int* arr = (int*)malloc(5 * sizeof(int));
free(arr);
5. 错误处理
new
- 如果内存分配失败,
new
会抛出std::bad_alloc
异常。 - 可以通过异常机制捕获分配失败的情况。
1
2
3
4
5
try {
int* p = new int[1000000000]; // 分配失败会抛出异常
} catch (std::bad_alloc& e) {
cout << "Allocation failed: " << e.what() << endl;
}
malloc
- 如果分配失败,
malloc
返回nullptr
。 - 程序员需要手动检查返回值。
1
2
3
4
int* p = (int*)malloc(1000000000 * sizeof(int));
if (!p) {
cout << "Allocation failed" << endl;
}
6. 优缺点对比
操作符/函数 | 优点 | 缺点 |
---|---|---|
new/delete | 1. 支持构造和析构函数,适合 C++ 面向对象编程。 2. 更类型安全,使用方便。 | 1. 仅适用于 C++。 2. 错误处理通过异常机制,可能增加复杂性。 |
malloc/free | 1. 与 C 兼容,可用于混合编程(C 和 C++)。 2. 错误处理简单,通过返回值判断。 | 1. 不支持构造和析构函数,需手动初始化和清理。 2. 类型不安全,需手动类型转换。 |
7. 总结
new/delete
是 C++ 的特有工具,更适合面向对象编程,能自动调用构造和析构函数。malloc/free
是 C 的标准函数,主要用于分配原始内存块,适合需要与 C 代码兼容的场景。- 在 C++ 中,建议优先使用
new/delete
,除非有特殊需求需要使用malloc/free
。