文章

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;
}

友元的优缺点

优点:

  1. 灵活性:友元允许对类的内部细节进行访问,适合实现复杂关系。
  2. 效率:直接访问成员变量,省去冗余的 getter 和 setter 函数。

缺点:

  1. 破坏封装:友元会增加耦合性,使类变得不那么独立。
  2. 维护复杂:友元关系增加了代码维护的复杂性。

友元的注意事项

  1. 友元关系是单向的,类 A 的友元类 B 并不意味着 A 是 B 的友元
  2. 友元关系不是继承的,派生类不会继承基类的友元。
  3. 友元函数和友元类定义在类的任何访问权限下都可以。

通过友元机制,可以平衡封装性与功能需求,但应谨慎使用以避免代码复杂度和耦合度的增加。

move函数

在 C++ 中,std::move 是一个标准库函数,用于实现 移动语义。它位于 <utility> 头文件中。std::move 并不是一个实际移动操作的函数,而是一个类型转换工具,它将一个对象显式地转换为一个右值引用(rvalue reference),从而可以将资源从一个对象转移到另一个对象,而无需昂贵的拷贝操作。


使用场景

  1. 避免拷贝:将资源从一个对象“移动”到另一个对象,而不是深拷贝数据。
  2. 优化性能:当对象内部包含动态资源(如堆内存、文件句柄等),移动操作可以通过资源转移提高效率。

移动语义和 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 使用注意事项

  1. 只用于临时转移资源: 一旦调用 std::move,原对象的状态通常会被设置为未定义或清空。不要再依赖原对象的值。
  2. 避免误用: 不要将 std::move 用在需要保留原对象状态的场景,否则可能引发错误。
  3. 与移动语义配合: 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++ 中的一种泛型编程工具,用于创建可以适应不同数据类型的类。它通过在类定义中引入类型参数,使得一个类可以操作多种类型,而无需为每种类型都定义一个单独的类。


模板类的作用

  1. 泛型编程:支持多种数据类型,避免代码重复。
  2. 代码复用:实现同一功能的类可以适配不同的数据类型,减少冗余代码。
  3. 类型安全:模板通过类型检查提供了更安全的代码结构。
  4. 灵活性:支持自定义的任意类型(用户定义类型、内置类型等)。
  5. 性能优化:与运行时的多态相比,模板通过编译时生成特定类型的代码,通常效率更高。

模板类的语法

模板类定义时使用关键字 template 声明类型参数。常见的类型参数包括:

  • T:常用作类型参数的占位符。
  • typenameclass:用来声明模板参数,功能相同(无语义区别)。

模板类基本定义:

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;
}

模板类与普通类的区别

特性普通类模板类
类型适配固定某一特定类型支持多种类型
代码复用性需要为每种类型重新编写类代码一套代码适配所有类型
性能固定编译代码编译时生成特定类型的代码,无额外开销
使用复杂性直接定义和使用需要显式指定类型参数

模板类的注意事项

  1. 编译时生成代码:每使用一种类型实例化模板时,编译器会生成一套对应的代码,因此滥用模板可能导致代码膨胀(编译时间和可执行文件大小增加)。
  2. 不能分离声明与实现(除非使用 template 关键字):模板的实现一般写在头文件中,因为编译器需要在实例化时看到所有模板定义。
  3. 类型限制:模板类默认支持所有类型,但如果某些操作不适用于某种类型,可能会引发编译错误。
  4. 特殊化和偏特化:可以为特定类型提供特殊实现。

总结

模板类是 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. 优势和劣势

模板的优势

  1. 编译期生成代码,无运行时开销。
  2. 支持高级特性,如偏特化、完全特化。
  3. 提供更强的灵活性,可以处理复杂的类型操作。

模板的劣势

  1. 编译时间长,可能导致代码膨胀。
  2. 错误信息复杂,模板调试难度大。

泛型的优势

  1. 设计思想更加通用,适用于多种语言。
  2. 在运行时适配类型,提升代码的适用性。

泛型的劣势

  1. 某些语言的泛型实现可能影响性能(如 Java 的类型擦除)。
  2. 对特定类型的优化能力较弱。

总结

  • 模板 是 C++ 中用于实现泛型编程的一种具体技术,通过编译时实例化实现高效代码。
  • 泛型 是一种更广义的编程范式,不局限于某种语言,用于设计和实现适配多种类型的算法和数据结构。
  • 两者关系:模板是实现泛型编程的一种技术手段,而泛型是一种编程思想。

内存管理:C++的new和malloc的区别

在 C++ 中,newmalloc 都可以用于动态分配内存,但它们的用途、功能和行为有显著区别。以下是详细对比:


1. 主要区别

特性newmalloc
语言支持是 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/delete1. 支持构造和析构函数,适合 C++ 面向对象编程。 2. 更类型安全,使用方便。1. 仅适用于 C++。 2. 错误处理通过异常机制,可能增加复杂性。
malloc/free1. 与 C 兼容,可用于混合编程(C 和 C++)。 2. 错误处理简单,通过返回值判断。1. 不支持构造和析构函数,需手动初始化和清理。 2. 类型不安全,需手动类型转换。

7. 总结

  1. new/delete 是 C++ 的特有工具,更适合面向对象编程,能自动调用构造和析构函数。
  2. malloc/free 是 C 的标准函数,主要用于分配原始内存块,适合需要与 C 代码兼容的场景。
  3. 在 C++ 中,建议优先使用 new/delete,除非有特殊需求需要使用 malloc/free
本文由作者按照 CC BY 4.0 进行授权