玩转CPP第一课

84 阅读10分钟

玩转CPP第一课

一.C++中的引用特性和 使用场景

C++ 中的引用(reference)是一个已经存在的对象的别名,它提供了对对象的直接访问,而不是对象的拷贝。以下是引用的主要特性和使用场景:

1. 引用特性

  1. 别名:引用是对象的别名,它不是一个新的对象,也不分配存储空间。它只是对象的一个已存在的内存地址的别名。
  2. 必须初始化:引用在声明时必须初始化,并且一旦初始化后就不能再指向其他对象。
  3. 无空引用:引用不能是 NULL 或者空(与指针不同)。
  4. 无引用引用:你不能创建一个引用的引用。
  5. 操作引用就像操作原对象:对引用的操作就像直接操作该对象一样。改变引用会反映到原对象上。
  6. 引用传递:在函数调用时,如果传递一个对象的引用,那么在函数内部就可以直接访问该对象,而不是对象的拷贝。这可以提高程序的性能,特别是当对象很大时。

2. 使用场景

  1. 函数参数:当函数需要修改传入的参数时,使用引用可以避免传递对象的拷贝,提高性能。例如,交换两个数的函数就需要使用引用。
void swap(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}
  1. 返回值优化:如果函数返回一个很大的对象,那么返回其引用(而不是值)可以避免拷贝构造函数的调用,提高性能。但需要注意的是,返回的引用必须是有效且持久的,否则可能会导致悬挂引用(dangling reference)。
const std::string& getString() {
    static std::string s = "Hello, World!";
    return s;
}
  1. 数据结构:在定义数据结构(如链表、树等)时,节点之间通常需要引用来维护彼此的关系。使用引用可以避免拷贝节点,减少内存使用和性能开销。
  2. 操作符重载:在操作符重载中,如果需要修改操作数,可以使用引用作为参数。
class MyClass {
    int value;
public:
    MyClass& operator+=(const MyClass& other) {
        value += other.value;
        return *this; // 返回当前对象的引用
    }
};
  1. 传递大对象或对象数组:对于大对象或对象数组,使用引用传递可以避免在函数调用时产生大量的拷贝开销。
  2. 常量引用:使用常量引用可以避免不必要的拷贝,并且保证原对象不会被修改。这在需要读取对象但不修改其内容的函数中非常有用。
void print(const std::string& str) {
    std::cout << str << std::endl;
}

总之,引用是 C++ 中一种非常有用的特性,它可以提供对对象的直接访问,减少不必要的拷贝开销,并提高程序的性能。但使用引用时也需要注意其生命周期和有效性的问题,以避免出现悬挂引用等问题。

二.C++的构造函数

C++ 中的构造函数可以按照它们接收的参数类型和用途来分类。下面是常见的几类构造函数以及它们的示例:

1. 默认构造函数(Default Constructor)

默认构造函数是无需任何参数就可以调用的构造函数。如果类中没有定义任何构造函数,编译器会自动提供一个默认构造函数。

class MyClass {
public:
    MyClass() {
        // 默认初始化
        std::cout << "Default constructor called" << std::endl;
    }
};

int main() {
    MyClass obj; // 调用默认构造函数
    return 0;
}

2. 带参数的构造函数(Parameterized Constructor)

带参数的构造函数允许在创建对象时提供初始化值。

class MyClass {
public:
    MyClass(int x) {
        value = x;
        std::cout << "Parameterized constructor called with " << x << std::endl;
    }

    int value;
};

int main() {
    MyClass obj(10); // 调用带参数的构造函数
    return 0;
}

3. 拷贝构造函数(Copy Constructor)

拷贝构造函数接受一个同类型的对象作为参数,用于创建该对象的副本。如果类中没有显式定义拷贝构造函数,编译器会提供一个默认的拷贝构造函数。

class MyClass {
public:
    MyClass(const MyClass& other) {
        value = other.value;
        std::cout << "Copy constructor called" << std::endl;
    }

    MyClass& operator=(const MyClass& other); // 注意:通常还需要定义赋值运算符

    int value;
};

// ... 拷贝构造函数的调用通常隐式进行,例如:
MyClass obj1(10);
MyClass obj2 = obj1; // 调用拷贝构造函数

// 或者显式调用:
MyClass obj3(obj1); // 显式调用拷贝构造函数

4. 移动构造函数(Move Constructor) (C++11 及更高版本)

移动构造函数用于转移另一个(临时或即将被销毁)对象的资源到当前对象,以避免不必要的拷贝操作。

class MyClass {
public:
    MyClass(MyClass&& other) noexcept {
        value = other.value;
        other.value = 0; // 或者其它“转移”操作
        std::cout << "Move constructor called" << std::endl;
    }

    int value;
};

MyClass ReturnObject() {
    MyClass temp(10);
    return temp; // 这里可能会调用移动构造函数(取决于编译器优化)
}

int main() {
    MyClass obj = ReturnObject(); // 可能调用移动构造函数
    return 0;
}

5. 委托构造函数(Delegating Constructor) (C++11 及更高版本)

委托构造函数允许一个构造函数调用同一个类中的另一个构造函数来执行初始化。

class MyClass {
public:
    MyClass(int x) {
        // ... 初始化代码 ...
    }

    MyClass(double d) : MyClass(static_cast<int>(d)) {
        // 委托给带 int 参数的构造函数
        std::cout << "Delegating constructor called" << std::endl;
    }
};

int main() {
    MyClass obj(3.14); // 调用带 double 参数的构造函数,然后委托给带 int 参数的构造函数
    return 0;
}

6. 转换构造函数(Conversion Constructor)

转换构造函数允许对象从其他类型隐式或显式地转换为该类类型。它们可能导致意外的类型转换,因此应该谨慎使用,并且可以通过将其声明为 explicit 来避免隐式转换。

class MyClass {
public:
    explicit MyClass(int x) {
        // ... 初始化代码 ...
    }
};

void foo(MyClass obj) {
    // ...
}

int main() {
    // MyClass obj = 42; // 错误,因为 MyClass(int) 是 explicit 的
    MyClass obj(42); // 正确,显式调用构造函数
    // foo(42); // 错误,不能从 int 隐式转换为 MyClass
    foo(MyClass(42)); // 正确,显式创建 MyClass 对象
    return 0;
}

三.C++的拷贝函数与析构函数

在C++中,拷贝构造函数(Copy Constructor)和析构函数(Destructor)是类的两个特殊成员函数,它们在对象的生命周期中扮演着重要的角色。然而,如果不正确地实现或使用它们,可能会引发一些问题。

1. 拷贝构造函数

功能:拷贝构造函数用于创建一个新对象,并将其初始化为另一个同类型对象的副本。

定义:拷贝构造函数通常具有一个参数,该参数是对同类型对象的引用。它的形式如下:

class MyClass {
public:
    MyClass(const MyClass& other); // 拷贝构造函数
    // ... 其他成员 ...
};

问题

2. 浅拷贝(Shallow Copy)与深拷贝(Deep Copy)

-   如果类包含动态分配的资源(如内存、文件句柄等),并且拷贝构造函数只复制了指针而不是实际的数据(浅拷贝),那么当原始对象被销毁时,其资源也会被释放,但拷贝的对象仍然持有指向已释放资源的指针,这会导致悬挂指针(dangling pointer)和未定义行为。为了避免这种情况,需要实现深拷贝,即分配新的资源并将数据复制到新分配的资源中。
  1. 不必要的拷贝

    • 在某些情况下,对象可能会不必要地被拷贝,例如在按值传递参数给函数或按值返回函数结果时。这可能导致性能问题,因为拷贝大对象可能会很耗时。可以使用引用传递或移动语义(C++11引入的移动构造函数和移动赋值运算符)来优化这种情况。

3.析构函数

功能:析构函数在对象生命周期结束时被调用,用于释放对象持有的资源并执行其他必要的清理工作。

定义:析构函数与类同名,并在名称前加上波浪号(~)。它没有参数,也没有返回值。形式如下:

class MyClass {
public:
    ~MyClass(); // 析构函数
    // ... 其他成员 ...
};

问题

  1. 资源泄漏

    • 如果析构函数没有正确释放对象持有的资源(如内存、文件句柄等),那么这些资源将不会被系统回收,导致资源泄漏。为了避免这种情况,需要确保在析构函数中释放所有动态分配的资源。
  2. 多次删除

    • 如果在对象的生命周期内多次调用析构函数(这通常是由于错误的资源管理导致的),可能会导致程序崩溃或其他未定义行为。这是因为析构函数会执行清理工作,如果它被执行多次,可能会试图删除同一个资源多次。
  3. 异常安全

    • 析构函数在销毁对象时可能会抛出异常。如果析构函数抛出异常且没有被捕获,程序会被终止。因此,在析构函数中执行可能引发异常的操作时要特别小心。通常建议析构函数不应抛出异常(或者应确保能够安全地处理这些异常)。

为了避免这些问题,需要仔细设计和实现拷贝构造函数和析构函数,确保它们正确地管理对象的资源,并遵循良好的编程实践。

3 友元函数(Friend Function)

友元函数是一个非成员函数,但它被声明为类的友元,因此它可以访问该类的私有(private)和保护(protected)成员。这种特性突破了类的封装性,因此应该谨慎使用友元函数。

3.1 特性

  1. 非成员函数:友元函数不是类的成员函数,因此它没有this指针。
  2. 访问权限:友元函数可以访问类的私有和保护成员。
  3. 声明方式:在类定义中,使用friend关键字声明一个函数或另一个类的所有成员函数为友元。

3.2示例

class MyClass {
private:
    int x;
public:
    MyClass(int val) : x(val) {}

    friend void printX(MyClass obj); // 声明友元函数
};

void printX(MyClass obj) {
    std::cout << obj.x << std::endl; // 可以访问私有成员x
}

int main() {
    MyClass obj(10);
    printX(obj); // 输出:10
    return 0;
}

4.静态函数(Static Function)

静态成员函数属于类本身,而不是类的某个对象。静态成员函数没有this指针,它们不能访问类的非静态成员(包括非静态成员函数和非静态数据成员),但可以直接访问类的静态成员。

4.1特性

  1. 属于类:静态成员函数属于类本身,而不是类的对象。
  2. this指针:静态成员函数没有this指针。
  3. 访问权限:静态成员函数可以访问类的静态数据成员和静态成员函数,但不能直接访问非静态成员。
  4. 调用方式:可以通过类名和作用域解析运算符::来调用静态成员函数,也可以通过类的对象来调用。

4.2示例

class MyClass {
private:
    static int count; // 静态数据成员
public:
    MyClass() { count++; } // 构造函数中更新静态数据成员
    ~MyClass() { count--; } // 析构函数中更新静态数据成员

    static void printCount() { // 静态成员函数
        std::cout << "Number of objects: " << count << std::endl;
    }
};

int MyClass::count = 0; // 静态数据成员的定义和初始化

int main() {
    MyClass obj1, obj2;
    MyClass::printCount(); // 输出:Number of objects: 2
    return 0;
}

在上面的示例中,MyClass有一个静态数据成员count,用于跟踪创建的对象数量。静态成员函数printCount可以访问和打印count的值。注意,静态数据成员需要在类外部进行定义和初始化。