Posts Modern-C++_语言基础
Post
Cancel

Modern-C++_语言基础

C++概述

C++特点: C++ 是一门多范式的通用编程语言。

C++支持”面向对象编程”、“泛型编程”、”函数式编程”:

  • 面向对象:核心在于抽象和封装,通过高内聚低耦合的对象之间的通信协作来完成任务。
  • 泛型编程:核心是“一切皆为类型”,通过模板而不是继承的方式来复用代码。
  • 函数式编程:核心是“一切皆可调用”,通过一系列连续或者嵌套的函数调用来实现对数据的处理。

生命周期和编程范式

一个 C++ 程序从“诞生”到“消亡”,要经历这么几个阶段:编码(Coding)、预处理(Pre-processing)、编译(Compiling)和运行(Running)。

  • 预处理: Pre-processor 执行,目的是文字替换。预处理指令都以符号#开头,应用于#include、#define、#if。#define宏定义,本身没有作用域概念,为全局生效。

  • 编译:完成的工作是”编译“和”链接“,生成计算机可识别的机器码。常用的特性为:“属性”和“静态断言”,

数据类型与关键字

  • const const定义的变量只能为整数或者枚举(#define宏定义没有数据类型只是简单的字符替换,无法进行类型检查)。const的作用是能 防止修改,增加程序健壮性 , 节省空间,避免不必要的内存分配 ,因为const定义常量从汇编的角度来看,只是给出了内存地址,而不是像#define一样给出的立即数,所以#define存在若干拷贝,而const不会。

非const变量默认为extern,而const对象默认为文件局部变量,如果需要访问,则需要显式声明extern,且需要初始化。const与指针:

如果const位于*的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;如果const位于*的右侧,const就是修饰指针本身,即指针本身是常量。

1
2
3
4
  const char * a; //指向const对象的指针或者说指向常量的指针。
  char const * a; //同上
  char * const a; //指向类型对象的const指针。或者说常指针、const指针。
  const char * const a; //指向const对象的const指针。
  • volatile 其含义是“不稳定的”、”易变的“,当 volatileconst 变量进行修饰后,编译器在生成二进制机器码的时候,不会做有副作用的优化:即编译器不会把 const value 做常量替换,而是对内存取值。 volatile 会禁止编译器做优化,因此除非必要,应当少用volatile。

  • mutable 修饰成员变量,允许 const成员函数 修改, mutable 变量的变化不影响对象的常量性,但是可能会“隐秘”地修改/损坏对象,尽量少用。

内存管理

基础概念

堆(heap):

指的是动态分配内存的区域,内存被分配之后需要手动释放,否则会造成内存泄漏。涉及两种分配/释放方式:

  • newdelete 操作的区域为 free store ,一般为堆的一个子集。

  • mallocfree 操作的区域为 heap

1
2
  // C++
  auto ptr = new std::vector<int>();

实际内存分配过程中,分配内存要考虑程序当前已经有多少未分配的内存。内存不足时要从操作系统申请新的内存。释放内存不只是简单地把内存标记为未使用。对于连续未使用的内存块,通常内存管理器需要将其合并成一块,以便可以满足后续的较大内存分配要求。

在不考虑垃圾回收情况下,内存需要手动释放,且内存存在碎片化的情况下(释放内存导致非连续内存无法被合并)。

栈(stack):

在内存管理的语境下,指的是函数调用过程中产生的本地变量和调用数据的区域。这个栈和数据结构里的栈高度相似,都满足“后进先出”( last-in-first-outLIFO )。

C++函数调用过程中,本地变量便是使用栈管理内存:任何一个函数,根据架构的约定,只能使用进入函数时栈指针向上部分的栈空间。当函数调用另外一个函数时,会把参数也压入栈里(我们此处忽略使用寄存器传递参数的情况),然后把下一行汇编指令的地址压入栈,并跳转到新的函数。新的函数进入后,首先做一些必须的保存工作,然后会调整栈指针,分配出本地变量所需的空间,随后执行函数中的代码,并在执行完毕之后,根据调用者压入栈的地址,返回到调用者未执行的代码中继续执行。本地变量所需的内存就在栈上,跟函数执行所需的其他数据在一起。当函数执行完成之后,这些内存也就自然而然释放掉了。

栈的特点:

  1. 栈上的分配极为简单,移动一下栈指针而已。

  2. 栈上的释放也极为简单,函数执行结束时移动一下栈指针即可。

  3. 由于后进先出的执行过程,不可能出现内存碎片。数据往往是容量确定大小的。

RAII(Resource Acquisition Is Initialization):

RAII ,全称“资源获取即初始化”,对 RAII 的使用,使得 C++ 不需要类似于 Java 那样的垃圾收集方法,也能有效地对内存进行管理。其核心思想是:将资源(如内存、文件句柄、锁等)的获取和释放绑定到对象的生命周期。

RAII 核心机制:

  1. 资源获取:在对象的构造函数中获取资源。

  2. 资源释放:在对象的析构函数中释放资源。

  3. 作用域管理:资源的生命周期由对象的作用域控制,当对象离开作用域时,其析构函数自动被调用,资源被正确释放。

RAII 的常见应用:

资源类型RAII实现方式标准实现
动态内存智能指针(std::uniqueptr, std::sharedptr)C++ 标准库支持
文件句柄自定义文件管理std::fstream
线程锁std::lockguard, std::uniquelockC++ 标准库支持
数据库连接数据库连接类第三方库支持
网络资源套接字管理类第三方库支持

左值和右值

  • 左值(lvalue)

  • 左值是指具有 地址 的表达式,可以持久存在。

  • 通常位于赋值运算符的左侧,但不绝对。

  • 可以通过引用来操作。

特点: 可寻址:可以取得其地址(通过 & 操作符)/可修改(如果不是常量)。

  • 右值(rvalue)

  • 右值是 临时值,不能通过引用直接访问地址。
  • 通常位于赋值运算符的右侧。
  • 右值可能是字面值、临时变量、表达式的结果。

特点:不可寻址:无法通过 & 取得其地址(除非通过类型转换)/短暂:右值的生命周期通常很短,通常仅在表达式求值期间有效。

使用场景:

  1. 引用绑定

C++提供两种引用类型:左值引用和右值引用。

1
2
3
  int a = 10;
  int& lref = a;      // 左值引用,绑定到变量 a
  int&& rref = 20;    // 右值引用,绑定到字面值 20

右值引用是绑定到右值的引用,通过 && 声明。它主要用于实现移动语义和完美转发(对应智能指针实现)。右值引用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  #include <iostream>
  #include <utility> // std::move
  using namespace std;

  class MyClass {
  public:
      MyClass() { cout << "Default Constructor" << endl; }
      MyClass(const MyClass&) { cout << "Copy Constructor" << endl; }
      MyClass(MyClass&&) { cout << "Move Constructor" << endl; }
  };

  int main() {
      MyClass obj1;             // 默认构造函数
      MyClass obj2 = obj1;      // 拷贝构造函数
      MyClass obj3 = std::move(obj1); // 移动构造函数
      return 0;
  }

C++引入了更精细的值类型划分:

值类别含义示例
左值(lvalue)持久存在的、可寻址的对象++x、x = 1、cout « ’ ‘ 字符串
右值(rvalue)短暂存在的值字面量、表达式结果等
纯右值(prvalue)只表示值本身,不关联存储位置x++、x + 1、makeshared(42)
亡值(xvalue)即将被销毁的对象(右值引用转移的结果),有标识符但无法被引用std::move(x) 的结果

指针

指针 源C语言,本质上是一个内存地址索引,代表一小片内存区域,支持直接读写内存;因为指针能够完全映射计算机硬件,效率高的同时也存在很多问题:访问无效数据、指针越界、内存无法释放等。

智能指针 基于 RAII 包装了裸指针,且重载了 *-> 操作符,用起来和原始指针一致。

智能指针分三类: unique_ptrshared_ptrweak_ptr ,智能指针实际上是对象,自动管理初始化时的指针,离开作用域后自动释放内存,也不能调用delete,且未初始化的指针默认为空指针,建议用工厂函数初始化。

  • uniqure_ptr

uniqure_ptr 需要基于C++的转移语义,同时禁止拷贝复制。 基本使用方法:

1
2
3
4
5
6
7
8
  auto ptr1 = make_unique<int>(12);
  assert(ptr1 && *ptr1 == 12);

  auto ptr2 = make_unique<string>("God of war.");
  assert(!ptr2->empty());

  auto ptr3 = std::move(ptr2);
  assert(!ptr2 && ptr3);  // move转移控制权后,ptr2成为空指针
  • shared_ptr

shared_ptruniqure_ptr 的区别在于:它的所有权是可以被安全共享的,即支持拷贝赋值,允许被多个人持有。 shared_ptr 支持安全共享的原理在于其内部使用了“引用计数”。 shared_ptr 的存储/管理成本更高,且存在循环引用的问题:

1
2
3
4
5
6
7
  auto ptr1 = make_shared<int>(43);
  assert(ptr1 && ptr1.unique());  // 此时指针有效且唯一

  auto ptr2 = ptr1;  // 支持直接拷贝赋值
  assert (ptr1 == ptr2); // 支持直接比较

  assert(!ptr1.unique() && ptr1.use_count() == 2); // 两个智能指针不唯一,且引用计数为2
  • weak_ptr

weak_ptr 能够解决循环引用的问题,赋值时不会添加引用计数,为弱引用,使用的时候通过 loca() 函数获取 shared_ptr 强引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  class Node final
  {
  public:
      std::weak_ptr<Node> next;
  }

  auto n1 = make_shaped<Node>();
  auto n2 = make_shaped<Node>();

  n1->next = n2;
  n2->next = n1; // 循环引用,如果shared_ptr,则循环引用次数变为2

  assert(n1.use_count() == 1); // weak_ptr,引用计数为1

  if (!n1->next.expired()) { // 检查指针是否有效
      auto ptr = n1->next.lock(); // lock获取shared_ptr
      assert(ptr == n2);
  }

C++类

类的基本概念:

  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
    
      // 默认构造函数
      class MyClass {
      public:
          MyClass() { /* 初始化 */ }  // 用户自定义默认构造函数
      };
    
      // 参数化构造函数
      class MyClass {
      public:
          MyClass(int value) : data(value) {}
      private:
          int data;
      };
    
      // 拷贝构造函数
      class MyClass {
      public:
          MyClass(const MyClass& other) { /* 拷贝数据 */ }
      };
    
      // 移动构造函数
      class MyClass {
      public:
          MyClass(MyClass&& other) noexcept { /* 转移资源 */ }
      };
    
  • 析构函数 析构函数能用于清理类对象占用的资源,编译器默认生成析构函数,但如果需要涉及管理动态资源,需要显式定义。

  • 拷贝赋值函数&&移动赋值函数 拷贝赋值函数完成一个对象赋值给另一个已经存在的对象,如果未显式定义,编译器会自动生成浅拷贝实现。

    1
    2
    3
    4
    5
    6
    7
    
    class MyClass {
    public:
        MyClass& operator=(const MyClass& other) {
            if (this != &other) { /* 防止自赋值 */ }
            return *this;
        }
    };
    

    移动赋值构造函数,是将一个临时对象转移到另一个对象中,可显著提高性能,避免不必要的资源拷贝。

    1
    2
    3
    4
    5
    6
    7
    
    class MyClass {
    public:
      MyClass& operator=(MyClass&& other) noexcept {
        if (this != &other) { /* 转移资源 */ }
          return *this;
      }
    };
    
  • 其他常用函数

    • 友元函数:用于特定情况下,允许非成员函数访问私有数据。它不属于类,但可以访问类的私有和保护成员。由于它是非成员函数,所以不能通过 this 指针访问对象。使用友元函数的场景:
      1. 特定情况下需要访问私有成员:提供类的内部数据与外部函数之间的接口。避免通过增加公共成员函数来暴露私有数据。

      2. 运算符重载的便利:友元函数常用于运算符重载(如 « 和 »),因为这些运算符无法作为类成员函数实现。

        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>
        class MyClass {
        private:
            int data;
        
        public:
            MyClass(int val) : data(val) {}
        
            // 声明友元函数
            friend std::ostream& operator<<(std::ostream& os, const MyClass& obj);
        };
        
        // 定义友元函数
        std::ostream& operator<<(std::ostream& os, const MyClass& obj) {
            os << obj.data;
            return os;
        }
        
        int main() {
            MyClass obj(42);
            std::cout << obj << std::endl; // 输出: 42
            return 0;
        }
        
    • constexpr构造: constexpr 构造函数是 C++11 引入的一种特殊构造函数,允许在编译时对对象进行初始化。它保证了构造函数是一个常量表达式,适用于编译期计算的场景。要求:构造函数的所有操作必须在编译器是可计算的,且数据成员必须能够在常量表达式中初始化。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      
      #include <iostream>
      class MyClass {
      private:
          int value;
      
      public:
          // constexpr 构造函数
          constexpr MyClass(int v) : value(v) {}
      
          // constexpr 成员函数
          constexpr int getValue() const { return value; }
      };
      
      int main() {
          constexpr MyClass obj(42); // 编译期构造对象
          constexpr int val = obj.getValue(); // 编译期获取值
                  
            std::cout << val << std::endl; // 输出: 42
            return 0;
        }
      
    • 委托构造函数 委托构造函数(Delegating Constructor)是 C++11 引入的一种功能,允许一个构造函数调用同一类中的另一个构造函数。目的是减少代码重复,简化构造函数的实现。

      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>
      class MyClass {
      private:
          int value1;
          int value2;
                  
      public:
          // 主构造函数
          MyClass(int v1, int v2) : value1(v1), value2(v2) {}
                  
          // 委托构造函数
          MyClass(int v) : MyClass(v, 0) {} // 调用主构造函数
                  
          void display() const {
              std::cout << "Value1: " << value1 << ", Value2: " << value2 << std::endl;
          }
      };
                  
      int main() {
          MyClass obj1(10, 20);
          obj1.display(); // 输出: Value1: 10, Value2: 20
                  
          MyClass obj2(42); // 调用委托构造函数
          obj2.display(); // 输出: Value1: 42, Value2: 0
          return 0;
      }
      
    • 重载运算符: 运算符重载允许开发者为用户定义的类型(如类和结构体)赋予类似内置类型的操作能力。这是通过定义特殊的运算符函数实现的。一些注意事项:
      1. 必须使用类的成员函数: 一元运算符(如 +)和赋值运算符必须是类的成员函数。
      2. 可以使用友元函数: 如 « 和 » 通常实现为友元函数。
      3. 不要滥用运算符重载: 确保重载运算符的语义符合预期。
  1. 虚函数与多态 虚函数(virtual function)是 C++ 提供的一种机制,用于支持运行时多态(也称为动态多态)。它允许派生类覆盖基类的成员函数,并在运行时根据实际对象类型选择合适的函数调用。在基类中使用 virtual 修饰的成员函数就是虚函数,派生类可以重写(覆盖)它。C++ 编译器会为类生成一个虚函数表,用于在运行时根据对象的类型调用正确的函数。调用虚函数时,程序会在运行时确定调用哪个版本的函数(基类的版本或派生类的版本)。

    虚函数的工作原理:

    虚函数表(vtable):

    • 每个含有虚函数的类都会生成一个虚函数表(vtable)。
    • 表中存储指向该类所有虚函数的指针。
    • 对象通过指针查找虚函数表,从而确定调用的函数。

    优点:实现动态多态,简化代码设计。支持接口抽象,适合面向对象的设计模式。

    缺点:运行时查找虚函数表有一定性能开销。增加类的内存开销:每个对象包含指向虚函数表的指针。增加了复杂性:需要注意析构函数是否为虚函数(防止基类函数指针删除派生类对象时,导致析构函数调用错误,引发内存泄漏)。

  2. 访问控制和特殊修饰符

    修饰符功能与应用
    public所有人都可访问
    protected本类和派生类可访问
    private仅本类可访问
    const成员函数不修改状态,变量或指针不可更改
    static类共享成员和成员函数,不依赖具体对象
    virtual支持动态多态
    override明确标识覆盖虚函数,防止签名错误
    final阻止函数被覆盖或类被继承
    mutable允许在常量成员函数中修改变量
    explicit防止隐式类型转换
    inline建议编译器将函数
This post is licensed under CC BY 4.0 by the author.