现代C++

文章目录

😍现代C++ 语言

​ C++ 是一个用户群体相当大的语言。从 C++98 的出现到 C++11 的正式定稿经历了长达十年多之久的积累。C++14/17 则是作为对 C++11 的重要补充和优化,C++20 则将这门语言领进了现代化的大门,所有这些新标准中扩充的特性,给 C++ 这门语言注入了新的活力。 那些还在坚持使用传统 C++ (本书把 C++98 及其之前的 C++ 特性均称之为传统 C++)而未接触过现代 C++ 的 C++ 程序员在见到诸如 Lambda 表达式这类全新特性时,甚至会流露出『学的不是同一门语言』的惊叹之情。

现代 C++ (本书中均指 C++11/14/17/20) 为传统 C++ 注入的大量特性使得整个 C++ 变得更加像一门现代化的语言。现代 C++ 不仅仅增强了 C++ 语言自身的可用性,auto 关键字语义的修改使得我们更加有信心来操控极度复杂的模板类型。同时还对语言运行期进行了大量的强化,Lambda 表达式的出现让 C++ 具有了『匿名函数』的『闭包』特性,而这一特性几乎在现代的编程语言(诸如 Python/Swift/… )中已经司空见惯,右值引用的出现解决了 C++ 长期以来被人诟病的临时对象效率问题等等。

​ C++17 则是近三年依赖 C++ 社区一致推进的方向,也指出了 现代C++ 编程的一个重要发展方向。尽管它的出现并不如 C++11 的分量之重,但它包含了大量小而美的语言与特性(例如结构化绑定),这些特性的出现再一次修正了我们在 C++ 中的编程范式。

​ 现代 C++ 还为自身的标准库增加了非常多的工具和方法,诸如在语言自身标准的层面上制定了 std::thread,从而支持了并发编程,在不同平台上不再依赖于系统底层的 API,实现了语言层面的跨平台支持;std::regex 提供了完整的正则表达式支持等等。C++98 已经被实践证明了是一种非常成功的『范型』,而现代 C++ 的出现,则进一步推动这种范型,让 C++ 成为系统程序设计和库开发更好的语言。Concept 提供了对模板参数编译期的检查,进一步增强了语言整体的可用性。

​ 总而言之,我们作为 C++ 的拥护与实践者,始终保持接纳新事物的开放心态,才能更快的推进 C++ 的发展,使得这门古老而又新颖的语言更加充满活力。1

C++是“面向过程、面向对象、泛型编程、编译性的静态强类型语言”。

一、前言

阅读本文需要传统C++基础。

PDF书籍:

现代 C++ 教程:高速上手 C++11/14/17/20

网站:

cplusplus.com

工具库 (lerogo.com)

C++ 参考手册 - C++中文 - API参考文档 (apiref.com)

序言 现代 C++ 教程: 高速上手 C++ 11/14/17/20 - Modern C++ Tutorial: C++ 11/14/17/20 On the Fly (changkun.de)

中文的C++ Template的教学指南。与知名书籍C++ Templates不同,该系列教程将C++ Templates作为一门图灵完备的语言来讲授,以求帮助读者对Meta-Programming融会贯通。(正在施工中)

问答:

C++面试-基础篇(全网最详细) - 知乎 (zhihu.com)

1.0 开始

1.0.1 关键字

绿色表示C++11增加的关键字。蓝色表示C++20增加的关键字。

类型 关键字 作用 备注
控制流 if 条件语句中的条件判断 在C++17后可以在if语句中定义局部对象。
控制流 else 条件语句中的否定分支
控制流 switch 多分支条件选择语句 C++17后可以在case中直接定义局部对象(在C中需要使用{})。
控制流 case switch 中的分支标签
控制流 default switch 中的默认分支 在switch表达式匹配不到任何case时,匹配default
循环 while
循环 do do-while循环
循环 for 已知次数的循环 C++11支持范围for循环。
循环 break 提前结束 结束当前循环或 switch 语句。
循环 continue 直接重开 结束当前循环的迭代,继续下一轮循环。
循环 goto 无条件转移语句 跳转到程序中指定的标号位置。常做错误处理。
函数 return 从函数中返回值 在现代编译器中存在返回值优化。
数据类型 bool 布尔类型 C++中bool占用一个字节,但 vector<bool> 却存在差异。
数据类型 char 字符类型 也常用于存储1字节的整数。
数据类型 short 短整数类型
数据类型 int 整数类型
数据类型 long 长整数类型
数据类型 float 单精度浮点数类型
数据类型 double 双精度浮点数类型
数据类型 void 空类型 通常用于函数无返回值或无参的情况。
数据类型 unsigned 无符号类型
数据类型 signed 有符号类型
数据类型 wchar_t 宽字符类型 也称为双字节类型、主要用在国际化程序的实现中。
数据类型 char16_t 16 位字符类型 由于wchar_t在不同系统环境字节数不一样,所以为了统一而出现。
数据类型 char32_t 32 位字符类型
数据类型 auto 自动类型推导 在C++11前表示自动存储类型,自C++11开始变更为自动数据类型。
存储类 static 静态类型 修饰成员函数和成员对象表示其独立于类的存在,其他用法与C一致
存储类 volatile 易变类型 表示数据是可变的、易变的,目的是不让 CPU 将数据缓存到寄存器,而是从原始的内存中读取。作用:改善编译器的优化能力。
存储类 extern 声明外部变量或函数 除与C一致的用法外,还可以使用extern "C"{},用于在 C++ 中调用 C 语言编写的函数或者在 C++ 中提供给 C 语言使用的接口。
存储类 register 寄存器类型 现期的编译器会自行决定局部变量的寄存器类型,该关键字在C++17后被遗弃、无任何作用,但任然保留以备他用。
存储类 thread_local 线程期对象 指示对象的生命周期为线程的生命周期期。每个线程拥有其自身的对象实例。
类型定义 typedef 定义类型别名
类成员修饰 mutable 易变对象 mutable是为了突破const成员函数无法修改成员对象的限制而设置。但也被用于lambda表达式中修改值传递的对象。
类成员修饰 virtual 定义虚函数 虚函数是C++的多态机制,使用virtual关键字声明。派生类重写后、允许通过基类指针访问基类与派生类的同名函数。
类成员修饰 explicit 显式构造函数声明 explicit关键字的作用就是防止类的单参数构造函数的隐式自动转换。
类成员修饰 friend 友元类或函数声明 突破类的访问权限限制,以访问任何成员。
类/成员修饰 final 终态修饰符 当应用于类时,表示该类是最终类,不能被其他类继承。
当应用于成员函数时,表示该函数是最终版本,不能被派生类重写。
类成员修饰 override 显示重写(覆盖) 当应用于派生类中的虚函数时,表示该函数是对基类虚函数的重写或覆盖
访问权限 public 公有成员访问权限
访问权限 private 私有成员访问权限
访问权限 protected 保护成员访问权限
构造类型 class 类的定义
构造类型 struct 结构体定义
构造类型 union 联合体定义
枚举类型 enum 枚举类型定义
命名空间 namespace 命名空间定义 细分全局作用域。
内存管理 new 动态分配内存 分配内存并调用构造函数。
内存管理 delete 释放动态分配的内存 调用析构函数并释放内存。
命名 using 模板类型别名 使用using可以很方便的完成对模板类型的别名定义。
运算符 sizeof 计算数据类型大小 编译期运算符
运算符 typeid 返回表达式的类型信息 静态类型语言,类型在编译时已经确定,但在多态时,无法在编译时确定对象的实际类型(基类指针或引用指向派生类对象)。
这是因为派生类可能在运行时进行创建和销毁,而我们需要在运行时才能获取实际类型信息。
运算符重载关键字 operator 定义运算符重载 为了使自定义的类型可以直接进行算数运算、比较、赋值、移位等操作。
类型转换 static_cast 静态类型转换 在具有相关性的类型之间转换(可以丢弃具备关联类型的CV限定符)。
类型转换 const_cast 常类型转换 只能添加或删除constvolatile属性。
类型转换 dynamic_cast 动态类型转换 用于将一个父类对象的指针/引用转换为子类对象的指针或引用(动态转换)。
类型转换 reinterpret_cast 重新解释转换 它会忽略类型之间的任何关联关系,通过重新解释内存内容来进行转换。但它却无法丢弃constvolatile属性。(自由度最高,风险性最高)
异常处理 throw 抛出异常 用于在程序中显式地抛出异常。
异常处理 try 异常处理尝试块 用于标记可能抛出异常的代码块。try块出现异常时,代码跳转到catch块中。
异常处理 catch 异常处理捕获块 用于捕获并处理异常。在 catch 块中,可以编写处理异常的代码逻辑。
空指针 nullptr 空指针常量 C++11后,用以解决NULL宏对因无法隐式转换导致的函数重载决议出现有违自觉的问题。
类型信息 alignof 查询对象对齐要求 alignof(int); // 4
用于获取指定类型的对齐系数,返回一个 size_t 类型的值。
类型检查 alignas 指定对象的对齐方式 struct alignas(int) S {};
用于设置指定对齐系数。alignas(N) 表示把绝对对齐系数设置为N字节,N必须是2的正数幂(1、2、4、16)。
编译期 static_assert 静态断言 编译期断言,不生成目标代码,因此不会造成任何运行期性能损失。(将错误排查提前到编译阶段。)
编译期 constexpr 常量表达式 指值不会改变并且在编译期就得到计算结果的表达式(函数)、仅表示支持在编译期求值(是否真的在编译期求值,不确定)。
编译期 consteval 编译期求值 要求表达式、函数必须在编译期求值
编译期 constinit 编译期初始化 修饰变量,保证变量在编译期初始化。(只能使用 constexprconsteval 函数初始化 constinit 变量)
类型推导 decltype 推导表达式的类型 在C++11时,auto已能对对象的类型进行推导,但无法对表达式的类型进行推导,decltype 随之产生。
异常规范 noexcept 指示函数不抛出异常 两种情况下应该使用noexcept:确信函数不会抛出 和(或)不知道如何(无法)处理异常。
模板 export 模板的外部定义 export用于模板的定义和声明分离的关键字。
然而,export关键字在实践中引入了复杂性,并且很少有编译器实现对其进行支持。因此,C++标准委员会决定在C++20标准中将其从语言规范中移除
模板 typename 在模板中引入类型名 typename T::SubType *ptr;
为了解决模板函数中使用模板类中的从属类(SubType 类型被定义在类T中)而产生。(示意T::SubType是T中的类型而不是T的成员)
模板 concept 模板实参约束 以在编译期检查模板实参是否满足指定的约束(解决模板函数无法处理某些特定类型的实例化进行限定)。
其他 asm 内嵌汇编语句
其他 inline 内联函数/变量 建议编译器在编译时将所修饰的函数代码直接嵌入到主调函数中。C++17后增加内联变量允许我们在头文件中定义变量,而无需担心多重定义错误。
其他 true 布尔的真值
其他 false 布尔的假值
其他 this 指向当前对象的指针 当类的成员函数的参数和类的成员同名时,为了区分这两个对象时产生。
其他 friend 类的友元 为其他的类(友元类)或函数(友元函数)突破该类的访问限定符而产生。

1.0.2 运算符

类别 运算符 结合性
后缀 () [] -> . ++ -- 从左到右
一元 + - ! ~ ++ -- (type) * & sizeof 从右到左
乘除 * / % 从左到右
加减 + - 从左到右
移位 << >> 从左到右
关系 < <= > >= 从左到右
相等 == != 从左到右
位与 AND & 从左到右
位异或 XOR ^ 从左到右
位或 OR ` `
逻辑与 AND && 从左到右
逻辑或 OR `
条件 ?: 从右到左
赋值 = += -= *= /= %= >>= <<= &= ^= ` =`
逗号 , 从左到右

1.0.3 发展史

C++ 的历史 (lerogo.com)

C++ 语言的发展历史可以分为以下几个阶段:

  1. C++ 1.0(1985年):作为 C 语言的扩展,加入了类、继承、多态等面向对象编程特性。
  2. C++ 2.0(1989年):加入了模板、异常处理、命名空间等新特性,并对标准库进行了重大改进。
  3. C++ 3.0(1991年):加入了运行时类型识别(RTTI)、抽象类、虚基类等特性,并且对异常机制进行了改进。
  4. C++ 11(2011年):加入了自动类型推导、lambda 表达式、右值引用、智能指针、线程库等新特性,并且对标准库进行了扩展。
  5. C++ 14(2014年):修复了一些 C++ 11 的问题,并加入了二进制字面量、泛型 Lambda 表达式、constexpr 函数等新特性。
  6. C++ 17(2017年):加入了结构化绑定、fold 表达式、if constexpr 等新特性,并对标准库进行了扩展。
  7. C++ 20(2020年):加入了概念(Concepts)、协程(Coroutines)、三方比较运算符(Three-way comparison operator)等新特性,并且对标准库进行了扩展。

C++各个标准的语法变更历史的概述:

C++ 89/C++98

  • 这是最初的 C++ 标准,引入了类、模板、异常处理等基本特性。

C++ 03

  • 主要是对 C++98 的一些技术错误和缺陷进行修正,并没有引入太多新的语法特性。

C++ 11

  • 引入了自动类型推导(auto 关键字)和区间循环(range-based for loop)。
  • Lambda 表达式:允许在代码中定义匿名函数。
  • 右值引用(rvalue reference)和移动语义:引入了 && 运算符,支持将资源的所有权从一个对象转移到另一个对象,提高了性能和效率。
  • 智能指针(smart pointers):引入了 std::shared_ptrstd::unique_ptr 等智能指针类,用于管理动态分配的内存。
  • 新的标准库组件:如并发编程库、正则表达式库、原子操作等。

C++ 14

  • 二进制字面量(binary literals):引入了以 0b0B 开头的二进制表示方法,例如 0b101010
  • 泛型 Lambda 表达式:Lambda 表达式可以使用模板参数。
  • constexpr 函数的扩展:constexpr 函数可以包含更多的语法,并且可以在运行时使用。
  • 基础字符串字面量(raw string literals):引入了原始字符串字面量,以 R"()" 的形式表示,可用于包含大段的原始文本。

C++ 17

  • 结构化绑定(structured bindings):允许直接解构元组或其他数据结构,将其成员绑定到变量中。
  • 折叠表达式(fold expressions):可用于简化模板元编程中的复杂表达式。
  • if constexpr:引入了编译时条件判断,用于在编译期间选择不同的代码分支。
  • 行内变量声明(inline variables):允许在头文件中定义并初始化变量(一种简洁且高效的方式来定义和共享全局变量)。
  • 并行算法:引入了一系列支持并行执行的标准算法。

C++ 20

  • 概念(Concepts):引入了对泛型编程中概念的支持,用于约束模板参数的类型。
  • 协程(Coroutines):引入了协程支持,允许在函数中暂停和恢复执行。
  • 三方比较运算符(Three-way comparison operator):引入了 <=> 运算符,用于支持三方比较操作。
  • 初始化上下文相关的内联变量(constinit):引入了 constinit 关键字,用于指示编译时非常量求值问题。

C++各版本的关键字变更历史

标准版本 关键字变更
C++89/C++98 auto, break, case, char, const, continue, default, do, double, else, enum, extern, float, for, goto, if, int, long, register, return, short, signed, sizeof, static, struct, switch, typedef, union, unsigned, void, volatile, while
C++03 bool, mutable, namespace, reinterpret_cast, static_cast, typename
C++11 alignas, alignof, char16_t, char32_t, constexpr, decltype, noexcept, nullptr, static_assert, thread_local
C++14 无新增关键字
C++17 if constexpr, inline variables
C++20 concept, consteval, constinit, co_await, co_return, co_yield, requires
C++23 (预计) import, module, export (从 C++20 移至 C++23), synchronized

请注意,以上是对各个 C++ 标准的关键字变更进行的概述,并不是详尽无遗的列表。每个标准还包含了其他的改进和修复,以提高语言的性能和可用性。此外,某些关键字的用法也可能发生变化,例如 C++11 引入的 auto 关键字的用法与 C++17 引入的 auto 关键字略有不同。

1.0.4 易混点

不同的方法或函数之间的关系
  • 隐藏(Hiding): 隐藏指的是在派生类中定义了与基类中同名的成员函数(方法),从而使得基类中的同名成员函数无法被派生类直接访问到。当派生类通过对象调用该同名成员函数时,实际上只会调用派生类中的成员函数,而不会调用基类中的同名函数。这种情况下,基类的同名成员函数被隐藏了。

      class Base {
         
      public:
        void display() {
         
          cout << "Base class display() function" << endl;
        }
      };
      
      class Derived : public Base {
         
      public:
        void display() {
         
          cout << "Derived class display() function" << endl;
        }
      };
      
      int main() {
         
        Derived d;
        d.display();  // 调用派生类的display()函数
        d.Base::display();  // 通过作用域解析运算符调用基类的display()函数
        return 0;
      }
      /*
      Derived class display() function
      Base class display() function
      */
    
  • 重写(Override): 重写是指派生类重新定义了继承自基类的虚函数。重写要求派生类的函数名称、参数列表和返回类型完全相同,并且使用关键字 override 明确表示重写基类的虚函数。重写的目的是为了实现多态性,即通过基类指针或引用调用派生类的函数时,能够根据对象的实际类型来调用相应的函数。

      class Shape {
         
      public:
        virtual void draw() {
         
          cout << "Drawing a shape" << endl;
        }
      };
      
      class Circle : public Shape {
         
      public:
        void draw() override {
         
          cout << "Drawing a circle" << endl;
        }
      };
    
  • 重载(Overload): 重载发生在同一个类(准确来说是同一作用域)中,指的是在一个类中定义了多个同名但参数列表不同的成员函数。重载函数具有相同的名称但不同的参数列表,可以根据传入的参数类型或个数的不同来调用不同的重载函数。重载函数之间的区别主要依靠参数列表,返回类型通常不是区分重载函数的条件。

      class Math {
         
      public:
        int add(int a, int b) {
         
          return a + b;
        }
      
        float add(float a, float b) {
         
          return a + b;
        }
      };
    
虚函数和纯虚函数
  • 虚函数(Virtual Functions)是在基类中声明的函数,可以在派生类中被重写(覆盖),不是必须要求重写。通过在函数声明前加上关键字 virtual 来定义虚函数。当通过基类的指针或引用调用虚函数时,将根据实际对象的类型来决定调用哪个版本的函数。

      class Base {
         
      public:
          virtual void foo() {
         
              cout << "Base::foo()" << endl;
          }
      };
      
      class Derived : public Base {
         
      public:
          void foo() {
         
              cout << "Derived::foo()" << endl;
          }
      };
    
  • 纯虚函数(Pure Virtual Functions)是在基类中声明的没有实际实现的虚函数。通过在函数声明后加上= 0来定义纯虚函数。纯虚函数在基类中没有具体的实现,但是派生类必须实现它们,否则派生类也会成为抽象类

    含有纯虚函数的类被称为抽象类,是不能被实例化的,只能作为基类来派生新的类使用。

      class Shape {
         
      public:
          virtual void draw() = 0;  // 纯虚函数
      };
      
      class Circle : public Shape {
         
      public:
          void draw() {
         
              cout << "Drawing a circle." << endl;
          }
      };
    
初始化列表

​ 这应该叫名称混淆点,因为这完全是两个不一样的东西,一个是语法,一个是类。不过在C++11以前,人们说的初始化列表都是构造函数中的冒号初始化。

  1. std::initializer_liststd::initializer_list 是C++11引入的一种特殊容器类型,它用于将一组值作为参数传递给函数或对象的构造函数。这种初始化列表使用花括号 {} 来表示,并且是只读的,不能修改其中的值。例如:

    std::initializer_list<int> numbers = {
         1, 2, 3};
    
  2. 冒号初始化列表:在类的构造函数中,可以使用冒号初始化列表来初始化成员变量。这种初始化列表出现在构造函数的定义之后,冒号之后,用于对成员变量进行初始化。例如:

    class MyClass {
         
    public:
        MyClass(int number) : m_number(number) {
         }
    private:
        int m_number;
    };
    

1.1 特点

C++不是C的一个超集(增强版)。

C++11 语言核心的改进中,最为关注的有 rvalue reference (这里),lambda,variadic template。

  1. C++ 不允许直接将 void * 隐式转换到其他类型的指针(C可以)。

  2. C++ 不允许隐式声明(而C允许,但仅仅出现警告,而这将产生难以发现的bug)。

    #include <stdio.h>
    
    int main(int argc, char *argv[])
    {
         
        int *i = malloc(sizeof(int)); 
        /* 使用编译命令(gcc -Wall)(于c标准无关,使用gcc -std2x -Wall,效果一致),提示警告(但可能无法运行)
         * warning: 函数' malloc '的隐式声明[-wimplicit-function-declaration]
         * warning: 内置函数' malloc'的不兼容隐式声明( 函数被隐式声明为:int malloc(unsigned int) )
         */
        /* 使用编译命令(g++ -Wall)提示错误并希望你加上头文件:#include <cstdlib>
         * error: ' malloc '没有在此范围内声明
         * 加上头文件后编译任然报错:error: 从'void* '到' int* '的转换无效[-fpermissive]
         */
        if (i == NULL) {
         
            printf("malloc failed\n");
            return 1;
        }
        return 0;
    }
    
  3. 乱序指定初始化、嵌套指定初始化、指定初始化器和正则初始化器的混合以及数组的指定初始化在C编程语言中都受支持,但在C++中不允许。

1.2 名词解释

  1. 静态类型语言:变量和表达式的类型必须在编译时声明,编译器可以帮助我们提前避免程序在运行期间有可能因为类型发生的一些错误。

  2. 强类型语言:不会处理与类型定义明显矛盾的运算,而是把它标记为一个问题,并作为错误抛出。

  3. 数组退化:当数组作为实参进行传递时会自动退化为指针(是一种隐式转换),传入数组的首地址。数组退化成指针后其「类型」「大小信息」将会丢失。

  4. 窄化转换:出现在强制转换中,表示对算术值出现了有精度的转换,不能完全表达转换之前所代表的值。

  5. 谓词(谓语):你之前一定在英语语法课上听过最基本的句式结构是:主谓宾,而谓语就是可以具有“动词”语义的词。其实在C++中也存在这样的谓词,函数就是典型的谓词语义。STL中的谓词类似这样:bool func(T&a);或者bool func(T&a,T&b);常见的谓词:函数、函数指针、函数对象、lambda表达式,库定义的函数对象。如std::find_if()std::count_if()等函数需要传入bool型的判断条件参数。

  6. 隐式声明:函数或对象仅有定义而没有进行声明;函数的定义在库(静态库或动态库中),但没有引入头文件。

  7. 匿名对象:不具有名称的对象。产生这种对象的方法通常是这样的:int(3)

  8. RVO(返回值优化):编译器可以减少函数返回时生成匿名对象的个数,从某种程度上可以提高程序的运行效率,对需要分配大量内存的类对象其值复制过程十分友好。

  9. NRVO(具名返回值优化)2NRVO优化的是返回指的对象在return语句前就已经构造完成(指在该函数体到return语句前完成构造的局部对象)。与RVO一样都是对函数内创建的返回值对象的优化、都是从函数返回值创建对象的优化、都是C++11之后才具有的编译器优化。

    可以使用如下编译选项禁用**(N)RVO**:

   -fno-elide-constructors
   
   int fun()  {
    return int(1);}
   auto fun1() {
   
       struct Obj
       {
   
           int a;
           Obj(int a): a(a){
   }
       };
       Obj a(1);
       return a;
   };
   
   void main(){
   
       int a = fun(); // RVO
       auto b = fun1();// NRVO
   }
  1. CV限定符:是 constvolatile 关键字的统称。

  2. 重载决议:是指编译器在调用函数或运算符时,根据参数类型、数量和顺序来选择最佳匹配的重载函数或运算符的过程。C++ 使用重载决议来确定应该调用哪个重载函数或运算符。如果没有找到完全匹配的函数或运算符,编译器将进行一系列的重载解析规则,以确定最佳匹配。

  3. 右值:指的是值可以被获取但不具有持久性的表达式。例如,常量、临时变量、字面值都是右值。右值可以作为函数的参数或返回值,但不能被赋值。是一个广义的概念,包括纯右值和将亡值。

    以下举例左值和右值的区别。简单来说,右值不可以被赋值,左值可以取地址。

    char i[4] = {
         1, 2, 3, 4};
    int main()
    {
         
        ++i[0] = 1; // 左值
        i[0]++ = 0; // 纯右值 error: lvalue required as left operand of assignment
        auto a = *reinterpret_cast<int *>(i) = 65536; // 左值
        &a = i + 3; // 纯右值 error: lvalue required as left operand of assignment
        
        i[2] + i[3] = 0; // 将亡值
    }
    /*
    ++i是左值,i++是纯右值
    解引用表达式 *p是左值,取地址表达式 &a 是纯右值。
    */
    
  4. 纯右值:是没有名称、不可寻址的表达式,通常是临时对象或字面量。

  5. 将亡值:是即将被销毁并且可以转移所有权的表达式,是特殊的右值。

    C++11引入的新概念,指的是即将被销毁并且可以转移其所有权的表达式,通常使用 std::move() 进行转移。将亡值通常是右值引用类型的变量、函数返回值或强制类型转换后的表达式。

    C++11中的将亡值是随着右值引用的引入而新引入的。换言之,“将亡值”概念的产生,是由右值引用的产生而引起的,将亡值与右值引用息息相关。所谓的将亡值表达式,就是下列表达式:

    • 返回右值引用的函数的调用表达式。
    • 转换为右值引用的转换函数的调用表达式。
  6. 右值引用:一种引用类型,用于绑定临时对象或将所有权转移给另一个对象。右值引用使用"&&"符号表示

  7. 悬垂引用:是指一个引用在其所引用的对象被销毁后依然存在。当一个对象被销毁时,与之相关的引用仍然存在,但指向的对象已经不存在了。使用悬垂引用是一种严重的错误,因为访问悬垂引用可能会导致未定义的行为。这是因为悬垂引用指向的内存可能已经被其他对象或数据覆盖,或者已经被释放回操作系统。

  8. 完美转发:是一种C++特性,模板函数可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变(保留原始参数的值和类型)。

  9. 引用折叠

  10. 万能引用

  11. 移动语义:将资源从一个对象转移到另一个对象,而不需要进行深拷贝操作。这对于管理动态内存或大型对象的效率非常重要。

  12. RTTI :(Run-Time Type Identification)运行时类型识别(RTTI)是一种机制,**用于在程序运行时获取和操作对象的类型信息。**主要有两种形式:dynamic_casttypeid运算符。注意,使用RTTI可能会导致运行时开销,因此需要谨慎使用。

    • dynamic_cast用于在运行时将指向基类的指针或引用转换为派生类的指针或引用。如果转换成功,则返回指向派生类的指针或引用;如果转换失败,则返回空指针(对指针进行转换)或引发std::bad_cast异常(对引用进行转换)。
    • typeid运算符:typeid用于在运行时获取对象的类型信息。它返回一个std::type_info对象,表示对象的实际类型。
  13. 常量求值语境:指在表达式中某值必须为常量的情况。

    // 在常量求值语境时,不同版本的getVal函数
    constexpr int getVal()
    {
         
        if consteval {
           // C++23 常量求值语境时执行的分支
            return 10;	// if consteval {}; consteval前后不能有圆括号,后面必须接花括号
        } 
        else{
         
            return 20;
        }
    }
    // C++14
    #include <utility>
    constexpr int getVal()
    {
         
        if (std::__is_constant_evaluated())  // C++14
            return 10;
        else
            return 20;
    }
    // C++20
    #include <utility>
    constexpr int getVal()
    {
         
        if (std::is_constant_evaluated())  // C++20
            return 10;
        else
            return 20;
    }
    
    // 以下均是常量求值语境
    static_assert(getVal() == 10);
    char a[getVal()];
    ...
        case getVal():...
    ...
    
  14. 立即函数:被关键字consteval修饰的函数,又称consteval函数。它必须在编译时被求值,并且不能包含任何运行时的代码。换句话说,consteval函数必须在编译时产生结果。

    // 立即函数
    #include <utility>
    consteval int f(int i) 
    {
          
        return i; 
    }
    
    constexpr int getVal(int a) // 本节 常量求值语境 中C++20版本的微改
    {
         
        if (std::is_constant_evaluated())
            return f(a) + 1; // error: call to consteval function 'f' is not a constant expression
        else
            return 20;
    }
    

    ​ 在语义角度看,if constexpr (std::is_constant_evaluated())已经限定了常量语境了,为何还不能调用立即函数?但是constexprconsteval的底层机理并不一致,所以并不相通。

    因此为解决这个问题,C++23 新增 if consteval { } 来代替 if(std::is_constant_evaluated())

  15. 默认参数提升:说的是参数提升的行为是被编译器默认的,而不是默认参数的提升。是指在函数调用时,当实参与函数原型中的形参类型不完全匹配时,编译器会向上进行隐式类型转换的过程。

    链接中详细提到了默认参数提升的细节:可变长参数列表误区与陷阱——va_arg不可接受的类型_c\c++ va_arg 用法不正确

    • float 类型的实际参数将提升到 double
    • charshort 和相应的signedunsigned 类型的实际参数提升到int
    • 如果 int 不能存储原值,则提升到unsigned int
    void foo(int a, double b = 3.14) {
         
        std::cout << "a = " << a << ", b = " << b << std::endl;
    }
    {
         
        char c = 'A';
        short s = 123;
        float f = 2.718f;
    
        foo(c);     // c被提升为int类型,使用默认参数3.14
        foo(s, f);  // s被提升为int类型,f被提升double
    }
    
    a = 65, b = 3.14
    a = 123, b = 2.718
    
  16. ODR

  17. 非类类型:指的是不是类(class)类型的类型,例如基本数据类型(如 intfloatchar 等)和枚举类型。这些类型在 C++ 中并没有封装成类(class)的形式,因此被称为非类类型。

1.3 约定

使用C++的规则

  1. 尽量不要使用 usign namespace std;

  2. 尽量使用{}初始化对象(能够使编译器检测到窄化转换)。

  3. 优先选择任务而不是线程。

  4. 优先使用智能指针,而不是原始指针。

  5. 不使用可能因未定义行为产生歧义的算法(未定义行为:语言标准未做规定的行为。例如:函数实参和同类运算符的计算顺序等)。

意图规则

  1. 在代码中直接表达思想(通过成员方法名称、返回值类型和STL算法等直接的表达意图)。
  2. 使用ISO C++标准编写代码(减少使用编译器扩展的C++功能,因为能够减少实现定义行为3)。

1.4 被弃用的特性

​ 被弃用不代表不能使用,而是暗示程序员这些特性在未来的标准中消失,应该避免使用(但是出于兼容性的考虑,大部分特性其实会被永久保留)。

  1. 不允许将字符串字面值常量赋值给char *(如:char *str="hello"),而是使用const char *str="hello"
  2. C++98 异常说明、 unexpected_handlerset_unexpected() 等相关特性被弃用,应该使用 noexcept
  3. auto_ptr 被弃用,应使用 unique_ptr
  4. register 关键字被弃用,可以使用但不再具备任何实际含义。
  5. bool 类型的 ++ 操作被弃用。
  6. 存在析构函数的类自动生成拷贝构造函数和拷贝赋值运算符的特性被弃用。
  7. C 语言风格的类型转换被弃用(即在变量前使用 (convert_type)),应该使用 static_castreinterpret_castconst_cast 来进行类型转换。
  8. 特别地,在 C++17 标准中弃用了一些可以使用的 C 标准库,例如 <ccomplex><cstdalign><cstdbool><ctgmath> 等。
  9. 分离编译模式无法对模板进行,标准 C++ 为此制定了“模板分离编译模式(Separation Model)”及 export 关键字。然而由于 template 语义本身的特殊性使得 export 在表现的时候性能很次,因此,该标准受到了几乎所有知名编译器供应商的强烈抵制。在较早的 C++ 草案中,export 关键字被用来表示模板的分离式定义,即模板的声明和定义可以分开写。然而,由于实现上的挑战和语义上的复杂性,C++ 标准委员会决定在 C++20 中移除对 export 关键字的支持。 实际上,这几乎没有影响,因为大多数编译器(MSVC、GCC)从不支持它。

1.5 语言可用性的强化

语言可用性是指发生在编译器前的语言行为。代指用户的体验:实用性(能用)、可用性(好用)、赞许性(推荐用)。

可用性分为:易学性、高效性、可记忆性、容错性、满意度。

  • 易学性:新手在第一次使用C++,是否可以轻松完成基本任务?
  • 高效性:当程序员学会使用你的产品后,他的使用效率有多高?
  • 可记忆性:当程序员在一段时间内不使用C++后,再一次使用C++时,他们的熟练度如何?
  • 容错性::程序员犯了多少错误,这些错误有多严重,以及他们从错误中恢复的难易程度?
  • 满意度:程序员使用C++的愉快程度如何?

1.5.1 常量

1.5.1.1使用 nullptr替代 NULL(C++11)

​ 在某种意义上来说,传统 C++ 会把 NULL0 视为同一种东西,这取决于编译器如何定义 NULL,有些编译器会将 NULL 定义为 ((void*)0),有些则会直接将其定义为 0

C++ 不允许直接将 void * 隐式转换到其他类型。因此下列代码不能在C++中完成编译。

#define NULL ((void*)0)

char *ch = NULL;

​ 没有了 void * 隐式转换的 C++ 只好将 NULL 定义为 0。而这依然会产生新的问题,将 NULL 定义成 0 将导致 C++ 中重载特性发生混乱。考虑下面这两个 foo 函数:

#define NULL (0)

void foo(char*){
   };
void foo(int){
   };

int main()
{
   
    foo(NULL); // 因此,这个语句将会去调用 foo(int)
}

​ 那么 foo(NULL); 这个语句将会去调用 foo(int),从而导致代码违反直觉。

​ 为了解决这个问题(1. 无法直接将 void * 隐式转换到其他类型;2. 重载特性发生混乱),C++11 引入了 nullptr 关键字,专门用来区分空指针、0。而 nullptr 的类型为 nullptr_t,能够隐式的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较。

1.5.1.2 使用constexpr修饰常量表达式(C++11)

常量表达式指编译期间能够确定结果的表达式、表达式(函数)的相同输入总会产生相同的输出,没有任何副作用(不会修改其他外部变量)。

编译器能够在编译时就把这些表达式直接优化并植入到程序运行时,将能增加程序的性能。

constexpr是一个加强版的const,它不仅要求常量表达式是常量,并且要求是一个编译阶段就能够确定其值的常量。

​ C++ 本身已经具备了常量表达式的概念,比如 1+2, 3*4 这种表达式总是会产生相同的结果并且没有任何副作用。 C++ 标准中数组的长度必须是一个常量表达式,而对于一个 const 常数作为数组长度的行为,(即便这种行为在大部分编译器中都支持,但是)是一个非法的行为。

int len = 10;
const int len_2 = len + 1;
constexpr int len_2_constexpr = 1 + 2 + 3;
// char arr_4[len_2];                // 非法
char arr_4[len_2_constexpr];         // 合法

注意,现在大部分编译器其实都带有自身编译优化,很多非法行为在编译器优化的加持下会变得合法,若需重现编译报错的现象需要使用老版本的编译器。

C++11 提供了 constexpr 让用户显式的声明函数或对象构造函数在编译期会成为常量表达式,这个关键字明确的告诉编译器应该去验证 被修饰的表达式(函数)在编译期就应该是一个常量表达式。

此外,constexpr 修饰的函数可以使用递归:

constexpr int fibonacci(const int n) {
   
    return n == 1 || n == 2 ? 1 : fibonacci(n-1)+fibonacci(n-2);
}

从 C++14 开始,constexpr 函数可以在内部使用局部变量、循环和分支等简单语句,例如下面的代码在 C++11 的标准下是不能够通过编译的:

不过在定义常量表示函数的时候,我们会遇到更多的约束规则[C++14]:

  1. 函数体允许声明变量,除了没有初始化、static和thread_local变量。
  2. 函数允许出现if和switch语句,不能使用goto语句。
  3. 函数允许所有的循环语句,包括for、while、do-while。
  4. 函数可以修改生命周期和常量表达式相同的对象(函数外部对象不能修改)。
  5. 函数的返回值可以声明为void。
  6. constexpr声明的成员函数不再具有const属性

另外还有些不允许的:

  1. 它必须非虚; [c++20前]
  2. 它的函数体不能是函数 try 块; [c++20前]
  3. 它不能是协程; [c++20起]
  4. 对于构造函数与析构函数 [C++20 起],该类必须无虚基类
  5. 它的返回类型(如果存在)和每个参数都必须是字面类型 (LiteralType)
  6. 至少存在一组实参值,使得函数的一个调用为核心常量表达式的被求值的子表达式(对于构造函 数为足以用于常量初始化器) (C++14 起)。不要求诊断是否违反这点。
constexpr int fibonacci(const int n) {
   
    if(n == 1) return 1;
    if(n == 2) return 1;
    return fibonacci(n-1) + fibonacci(n-2);
}

​ 为此,我们可以写出下面这类简化的版本来使得函数从 C++11 开始即可用:

constexpr int fibonacci(const int n) {
   
    return n == 1 || n == 2 ? 1 : fibonacci(n-1) + fibonacci(n-2);
}

​ 在C++17标准中,constexpr声明静态成员变量时,也被赋予了该变量的内联属性。

class X 
{
   
    public:
    static constexpr int num{
    5 };
};

以上代码从C++17开始等价于:

class X 
{
   
    public:
    // X::num既是声明又是定义(无需(也不能)在外部再定义constexpr int X::num=5)
    inline static constexpr int num{
    5 }; 
};

1.5.2 变量及其初始化

1.5.2.1 if/switch变量声明强化(C++17)

C++17 之后可以在if/switch语句中声明局部变量,使得我们可以在 if(或 switch)中完成很多操作。

语法结构:

if (<变量声明>; <条件语句>)
{
   
    // 语句块
}

例子:

// 将临时变量放到 if 语句内
if (const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 3);
    itr != vec.end()) {
   
    *itr = 4;
}
1.5.2.2 初始化列表(C++11)

​ 在C++11以前(传统 C++ ),不同的对象有着不同的初始化方法,例如普通数组、POD (Plain Old Data,即没有构造、析构和虚函数的类或结构体)类型都可以使用 {} 进行初始化,也就是我们所说的初始化列表。而对于类对象的初始化,要么需要通过拷贝构造、要么就需要使用 () 进行。这些不同方法都针对各自对象,不能通用。

为解决这个问题(需要使用不同方式去初始化对象,而没有统一的办法),C++11 首先把初始化列表的概念绑定到类型上,称其为 std::initializer_list(除了用在对象的构造之外,还能用于函数的形参),允许构造函数或其他函数像参数一样使用初始化列表,这就为类对象的初始化与普通数组和 POD 的初始化方法提供了统一的桥梁。

#include <initializer_list>
#include <vector>
#include <iostream>

class MagicFoo {
   
public:
    std::vector<int> vec;
    MagicFoo(std::initializer_list<int> list) {
   
        for (std::initializer_list<int>::iterator it = list.begin();
             it != list.end(); ++it)
            vec.push_back(*it);
    }
};
int main() {
   
    // after C++11
    MagicFoo magicFoo = {
   1, 2, 3, 4, 5};

    std::cout << "magicFoo: ";
    for (std::vector<int>::iterator it = magicFoo.vec.begin(); 
        it != magicFoo.vec.end(); ++it) 
        std::cout << *it << std::endl;
}

C++11 还提供了统一的语法来初始化任意的对象(即:使用{}进行初始化)

#include <iostream>
#include <vector>

class Foo {
   
public:
    int value_a;
    int value_b;
    Foo(int a, int b) : value_a(a), value_b(b) {
   }
};

int main() {
   
    // before C++11
    Foo foo(1, 2);
	// C++11
    Foo foo2{
   11, 22}; // 可以用其初始化任意对象
    return 0;
}
1.5.2.3 结构化绑定(C++17)

​ **结构化绑定提供了类似其他语言中提供的多返回值的功能。**在容器一章中,我们会学到 C++11 新增了 std::tuple 容器用于构造一个元组,进而囊括多个返回值。但缺陷是,C++11/14 并没有提供一种简单的方法直接从元组中拿到并定义元组中的元素,尽管我们可以使用 std::tie 对元组进行拆包,但我们依然必须非常清楚这个元组包含多少个对象,各个对象是什么类型,非常麻烦。

C++17 完善了这一设定,给出的结构化绑定可以让我们写出这样的代码:

#include <iostream>
#include <tuple>

std::tuple<int, double, std::string> f() {
   
    return std::make_tuple(1, 2.3, "456");
}

int main() {
   
    auto [x, y, z] = f();
    std::cout << x << ", " << y << ", " << z << std::endl;
    return 0;
}

1.5.3 类型推导(C++11)

​ 在传统 C 和 C++ 中,参数的类型都必须明确定义,这其实对我们快速进行编码没有任何帮助,尤其是当我们面对一大堆复杂的模板类型时,必须明确的指出变量的类型才能进行后续的编码,这不仅拖慢我们的开发效率,也让代码变得又臭又长。

C++11 引入了 autodecltype 这两个关键字实现了类型推导,让编译器来操心变量的类型。这使得 C++ 也具有了和其他现代编程语言一样,某种意义上提供了无需操心变量类型的使用习惯。

1.5.3.1 auto

auto 在很早以前就已经进入了 C++,但是他始终作为一个存储类型的指示符存在,与 register 并存。在传统 C++ 中,如果一个变量没有声明为 register 变量,将自动被视为一个 auto 变量。而随着 register 被弃用(在 C++17 中作为保留关键字,以后使用,目前不具备实际意义),对 auto 的语义变更也就非常自然了。

关键字auto的变化历史

  • 传统C++和C:表示对象(变量)的存储类型。
  • C++11:更改了auto关键字的意义,能自动推导对象类型和返回值类型(需要结合-> decltype(表达式)构成尾返回类型)。
  • C++14:auto可以对函数的返回值类型直接进行推导(不在需要-> decltype(表达式)的尾返回类型),并且结合decltype(auto) 用于对转发函数或封装的返回类型进行推导。
  • C++20:在函数的形参中使用,用于模版的简化写法。

注意:auto还不能推导数组类型。

auto auto_arr2[10] = {arr}; // 错误, 无法推导数组元素类型

使用 auto 进行类型推导的一个最为常见而且显著的例子就是迭代器。

#include <initializer_list>
#include <vector>
#include <iostream>

class MagicFoo {
   
public:
    std::vector<int> vec;
    MagicFoo(std::initializer_list<int> list) {
   
        // 从 C++11 起, 使用 auto 关键字进行类型推导
        for (auto it = list.begin(); it != list.end(); ++it) {
   
            vec.push_back(*it);
        }
    }
};
int main() {
   
    MagicFoo magicFoo = {
   1, 2, 3, 4, 5};// C++11 初始化列表
    std::cout << "magicFoo: ";
    // C++11
    for (auto it = magicFoo.vec.begin(); it != magicFoo.vec.end(); ++it) {
   
        std::cout << *it << ", ";
    }
    // before C++11
    for (std::vector<int>::iterator it = magicFoo.vec.begin(); it != magicFoo.vec.end(); ++it) {
   
        std::cout << *it << ", ";
    }
    std::cout << std::endl;
    return 0;
}

从 C++ 20 起,auto 甚至能用于函数传参,考虑下面的例子:

int add(auto x, auto y) {
   
    return x+y;
}

auto i = 5; // 被推导为 int
auto j = 6; // 被推导为 int
std::cout << add(i, j) << std::endl;
1.5.3.2 decltype(C++11)

decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的(decltype能对表达式进行类型推导)。它的用法和 typeof 很相似:


auto x = 1;
auto y = 2;
decltype(x+y) z; //  decltype用于推断类型的用法

// 判断上面的变量 `x, y, z` 是否是同一类型
if (std::is_same<decltype(x), int>::value)
    std::cout << "type x == int" << std::endl;
if (std::is_same<decltype(x), float>::value)
    std::cout 
现代C++程序设计(原书第2版)》图文并茂,通俗易懂,真正做到寓教于乐,是一本难得的C++面向对象设计入门教材。 出版者的话 译者序 前言 第1章 C++概述与软件开发 1.1 什么是C语言和C++ 1.1.1 C和C++历史回顾 1.1.2 C/C++是一门编译语言 1.1.3 为什么许多程序员都选择C++ 1.2 什么是面向对象 1.2.1 C++程序并不一定是面向对象的 1.2.2 一个简单的面向对象程序示例 1.2.3 面向对象的软件更有优势 1.3 结构化设计与面向对象设计 1.3.1 ATM——结构化设计 1.3.2 采用面向对象方法的ATM——究竟是谁的任务 1.3.3 汽车维护——结构化设计 1.3.4 采用面向对象方法的汽车维护——究竟是谁的任务 1.4 软件开发技术概述 1.5 问题发现与解决 复习题 第2章 C++的入门知识 2.1 编程基础 2.1.1 算法设计 2.1.2 正确的软件开发步骤 2.2 专业术语及工程创建 2.3 C++程序的一般格式 2.3.1 “Hello World!”程序 2.3.2 “How’s the Weather?”程序 2.4 程序的数据及数据类型 2.4.1 C++的数据类型 2.4.2 容器=数据类型,标签=变量名 2.4.3 数据类型修饰符 2.4.4 问题分析:整型数据究竟有多大 2.5 C++中的变量声明 2.5.1 C++的命名规则 2.5.2 在哪里声明变量 2.6 C++中的运算符 2.6.1 计算路程的程序 2.6.2 从键盘输入程序所需数据 2.6.3 赋值运算符 2.6.4 运算符的优先级 2.6.5 数据类型及其存储的值 2.6.6 算术运算符 2.6.7 自增运算符和自减运算符 2.6.8 复合赋值运算符 2.7 #define、const和数据类型转换 2.7.1 #define预处理指令 2.7.2 const修饰符 2.7.3 const比#define好吗 2.7.4 数据类型转换 2.8 关于键盘输入和屏幕输出的更多内容 2.8.1 转义序列 2.8.2 ios格式标记 2.8.3 流的IO控制符 2.9 开始使用类和对象、C++string类 2.10 练习 复习题 第3章 控制语句和循环 3.1 关系运算符和逻辑运算符 3.2 if语句 3.2.1 if-else语句 3.2.2 问题分析:在if语句中使用大括号 3.2.3 if-else if-else语句 3.2.4 低效的编程方法 3.2.5 if-else程序示例 3.2.6 嵌套if-else语句 3.2.7 条件运算符“?” 3.3 switch语句 3.4 循环 3.4.1 括号的用法 3.4.2 无限循环 3.5 for循环 3.5.1 不要改变循环索引 3.5.2 for循环示例 3.6 while循环 3.7 do while循环 3.8 跳转语句 3.8.1 break语句 3.8.2 continue语句 3.9 问题发现与解决 3.9.1 五个常见错误 3.9.2 调试程序 3.10 C++类与vector类 3.11 总结 3.12 练习 复习题 第4章 函数一:基础 4.1 C++中的函数 4.1.1 只由一个main函数构成的程序 4.1.2 包含多个函数的程序 4.1.3 函数是个好东西 4.1.4 三个重要的问题 4.2 函数:基本格式 4.3 函数的编写要求 4.3.1 你想住在C++旅馆中吗 4.3.2 函数为先 4.3.3 函数声明或函数原型 4.3.4 函数定义、函数标题行与函数体 4.3.5 函数调用 4.3.6 传值调用 4.3.7 问题分析:未声明的标识符 4.4 重载函数 4.5 具有默认输入参数列表的函数 4.6 局部变量、全局变量和静态变量 4.6.1 局部变量 4.6.2 块范围 4.6.3 全局变量 4.6.4 危险的全局变量 4.6.5 问题分析:全局变量y0、y1与cmath 4.6.6 静态变量 4.7 C++stringstream类 4.8 总结 4.9 练习 复习题 第5章 函数二:变量地址、指针以及引用 5.1 数据变量和内存 5.1.1 sizeof运算符 5.1.2 预留内存 5.1.3 计算机内存和十六进制 5.2 取地址运算符& 5.3 指针 5.4 函数、指针以及间接运算符 5.4.1 解决思路 5.4.2 指针和函数 5.4.3 有效处理大型数据 5.5 函数和引用 5.5.1 复习:两种机制 5.5.2 为什么要强调指针的重要性 5.6 queue类 5.7 总结 5.8 练习 复习题 第6章 数组 6.1 使用单个数据变量 6.2 数组基础 6.2.1 数组的索引值从0开始 6.2.2 使用for循环和数组来实现的电话账单程序 6.2.3 数组的声明和初始化 6.2.4 数组越界==严重的问题 6.2.5 vector与数组的比较 6.3 数组和函数 6.3.1 每个数组都有一个指针 6.3.2 数组指针 6.3.3 向函数传递数组:最开始的引用调用 6.3.4 利用数组和函数生成随机数并进行排序 6.4 C字符串,也称为字符数组 6.4.1 字符数组的初始化 6.4.2 null字符 6.4.3 C字符串的输入 6.4.4 C++中提供的字符数组函数 6.5 多维数组 6.5.1 二维数组的初始化 6.5.2 嵌套的for循环和二维数组 6.5.3 利用二维数组来实现Bingo游戏 6.6 多维数组和函数 6.6.1 改进的Bingo卡片程序 6.6.2 白雪公主:利用二维数组来存储姓名 6.7 利用数据文件对数组赋值 6.8 总结 6.9 练习 复习题 第7章 类和对象 7.1 我们所了解的类和对象 7.2 编写自己的类 7.2.1 入门实例:自定义日期类 7.2.2 第一个C++类:Date类 7.2.3 揭开类的生命之谜 7.2.4 set和get函数的作用与VolumeCalc类 7.2.5 PICalculator类 7.3 作为类成员的对象 7.4 类的析构函数 7.5 对象数组 7.6 重载运算符与对象 7.7 指针、引用和类 7.7.1 指针和引用实例 7.7.2 处理日期和时间的程序实例 7.8 总结 7.9 练习 复习题 第8章 继承和虚函数 8.1 为什么继承如此重要 8.1.1 IceCreamDialog实例 8.1.2 Counter类实例 8.2 继承基础 8.2.1 Counter和DeluxeCounter实例 8.2.2 保护成员 8.2.3 员工、老板和CEO 8.3 访问控制符的规范和多继承 8.4 继承、构造和析构 8.4.1 构造函数和析构函数回顾 8.4.2 基类和派生类的默认构造函数——没有参数 8.4.3 在重载的构造函数中使用参数 8.4.4 基类和派生类的析构函数 8.4.5 医生也是人 8.4.6 关于派生类和基类构造函数的规则 8.5 多态和虚函数 8.5.1 多态——同一个接口,不同的行为 8.5.2 什么是虚函数 8.5.3 虚函数的作用 8.6 总结 8.7 练习 复习题 附录A 学习使用Visual C++2005Express Edition 附录B C++关键字表 附录C C++运算符 附录D ASCII码 附录E 位、字节、内存和十六进制表示 附录F 文件输入/输出 附录G 部分C++类 附录H 多文件程序 附录I Microsoft visual C++2005Express Edit
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值