【啃书系列】【C++ Primer Plus】【二】程序结构

二、程序结构

1. 循环

  • x++和++x

    1. 当x是内置类型时,没有差别

    2. 当x是用户定义的类型时,++x效率更高,因为x++的执行步骤为:复制一个副本,将其加1,将复制的副本返回

      x++、x += 1、x = x + 1的执行效率

      x = x + 1最低,因为它的执行过程如下:

      (1)读取右x的地址

      (2)x + 1

      (3)读取左x的地址

      (4)将右值传给左边的x(编译器并不认为左右x的地址相同)

      x += 1其次,其执行过程如下:

      (1)读取右x的地址

      (2)x + 1

      (3)将得到的值传给x(因为x的地址已经读出)

      x++效率最高,其执行过程如下:

      (1)读取右x的地址

      (2)x自增1

  • 测试用户输入合法性时可以使用do while语句

  • 读取char值时,cin会忽略掉空格和回车,不愿忽略可以使用cin.get(ch); // ch实时改变
    结束可以用cin.eof()cin.fail(),但是对于键盘输入来说可以用cin.clear();清除标记继续读取(部分系统)

    while (cin.get(ch)){
        ...
    }
    
  • 逗号的一些情况示例(优先级最低

    cats = 17, 240;		// 等价于(cats = 17), 240;赋值为17
    cats = (17, 240);	// 赋值为240
    

2. 分支、逻辑运算符

  • C++中也可以用not、and、or,方法和python里一样

  • 字符函数库cctype

    #include<cctype>
    isalpha();
    isdigit();
    isspace();		// 检测是否为空白' ', '\n', '\t'
    ispunct();
    tolower();		// 如果是大写字符,则返回其小写,否则返回该参数
    toupper();		// 如果是小写字符,则返回其大写,否则返回该参数
    isupper();		// 如果是大写字母,返回true
    
  • if-else和switch都能使用时,如果选项大于等于3个,用switch更快

  • int类型cin入一个字母会怎么样?

    int n;
    cin >> n;
    
    1. n的值保持不变
    2. 不匹配的输入将被留在输入队列中
    3. cin对象中的一个错误标记被设置
    4. 对cin方法的调用将返回false

    而后可以用cin.clear();继续接收输入

3. 简单文件输入/输出

  • 读取整行带空格不带回车的字符时使用cin.getline(name, size);

  • 写入到文本文件与控制台输出极为类似

    需要十分注意程序的健壮性

    #include<fstream>				// fstream中定义了一个用于处理输出的ofstream类
    #include<cstdlib>
    ofstream fout;
    
    char filename[50];
    cin >> filename;
    fout.open(filename);
    
    char line[80] = "I love C++";
    fout << line << endl;			// 和cout一样,有<<、endl、setf()等
    
    fout.close();					// 最后需要关闭文件
    
    #include<fstream>				// fstream中定义了一个用于处理输入的ifstream类
    ifstream fin;
    
    fin.open(filename);
    /*********重要**********/
    if (!fin.is_open()){			// 文件打开失败的情况
        exit(EXIT_FAILURE);			// 定义在cstdlib中
    }
    /***********************/
    fin.getline(line, 80);
    
    什么时候文件会打开失败?
    1. 指定的文件可能不存在
    2. 文件可能位于另一个目录(文件夹)中
    3. 访问可能被拒绝
    4. 用户可能输错了文件名或省略了扩展名
  • 小总结:fstream中定义的文件中有输入输出两个类,分别定义对象,然后像cin和cout一样使用即可

4. 函数

  • C++函数返回值不能是数组,但可以是其它任何类型

    函数是如何返回值的?
    1. 函数将返回值复制到指定的CPU寄存器或内存单元中
    2. 调用程序查看该内存单元
    3. 返回函数和调用函数就该内存单元中存储的数据类型达成一致
  • 传递常规变量时,函数将使用该变量的拷贝;但传递数组时,函数将使用原来的数组(首地址指针)

    // 假设4字节地址系统
    void func(int a[], int n){
        cout << sizeof(a);				// 输出的是4
    }
    int main(){
        int cookies[8];
        cout << sizeof(cookies);		// 输出的是32
        func(cookies, 8);
    }
    

    如果我们要实现数组的**“值传递”,那么可以将形参改为const int a[]**

    const和constexpr的区别
    1. const并未区分出编译期常量和运行期常量

    2. constexpr限定在了编译期常量

    总的来说,编译期就能算出来,可以提高运行的效率,能用constexpr就用constexpr

  • const + 指针

    禁止将const指针赋值给非const指针

    1. 指针指向常量**const int * ps = &sloth;**

      • 可以修改指向的对象(指向另一个对象)
      • 不能通过该指针修改指向对象的值
      • 仅当只有一层间接关系(如指针指向基本数据类型)时,才可以将非const指针赋值给const指针
    2. 常量指针指向变量**int * const finger = &sloth**

      • 不能修改指向的对象(指向另一个对象)
      • 可以通过该指针修改指向对象的值
    3. 常量指针指向常量**const double * const stick = &trouble**

      • 易得,什么都改不了

    小总结const int *是指向常量,所以不能该指向对象的值;int * const是指针本身为常量,所以不能修改指向的对象(专一)

  • 二维数组传参

    // 方式1,用数组指针(指向指针的指针,所以不用const)
    int sum(int (*arr)[4], int n);
    // 方式2(第二维的大小无法省略)
    int sum(int arr[][4], int n);
    // 由以上两种方法都可知:arr形参是指针,因此可以如下方式取值
    int a = *(*(arr + row) + col);
    

5. *函数指针

函数的地址是存储其机器语言代码的内存的开始地址

  • 函数的地址:函数名就是函数的地址

  • 声明函数指针

    函数指针就是用(*name)替换掉函数原型中的函数名

    double pam(int);		// 函数原型
    // 正确函数指针声明如下
    double (*pf)(int);		// 函数指针,可以想到函数签名,用最少的量表明完整的一个函数
    // double *pf(int);		// 这只是一个返回值是double指针的函数原型
    pf = pam;				// 函数签名必须一样,否则无法赋值!
    
  • 使用指针来调用函数

    // 第一种方式
    double y = (*pf)(5);	// 虽然比较复杂,但是说明了这是个函数指针
    // 第二种方式
    double y = pf(5);
    

我们再来深究一下

  • 下面三个函数的原型特征标和返回类型相同

    // 在形参中,数组就是指针
    // 返回值是const double *,也就是一个指向常量的指针
    const double * f1(const double a[], int n);
    const double * f2(const double [], int);
    const double * f3(const double *, int);
    // 函数指针
    const double *(*pf1)(const double *, int) = f1;
    // 当然,我们可以这样!
    auto pf2 = f2;
    
  • 但是这样函数太多了,所以我们不妨试一试指针函数数组

    // TODO(可以把所有排序算法用函数指针数组写一遍)

    const double * (*pf1)(const double *, int) = f1;		// 这是一个函数指针
    auto pf2 = f2;
    auto pf3 = f3;											// 或者是这样
    // auto自动类型推断只能用于单值初始化,而不能用于初始化列表
    // 因为这首先是个指针数组,所以*pf_array1[3]表明了指针数组
    const double * (*pf_array1[3])(const double *, int);	// 这是一个函数指针数组
    // 当有了一个数组后,一切都简单了
    auto pf_array2 = pf_array1;
    

    那么如何调用这些函数呢?

    其实和普通指针是一样的

    double a = *(*pf_array1[2])(p, 3);			// 对应上面double y = (*pf)(5);
    double b = *pf_array1[2](p, 3);				// 对应上面double y = pf(5);
    // 最左边那个*的原因是函数返回值本身是个指针,要取值的话就得加*
    

    // TODO 函数指针对性能有什么影响吗?

  • 那么函数数组指针呢?

    // 和指针数组、数组指针一个道理
    const double * (*(*array1_pf)[3])(const double *, int);
    // 但是我选择auto
    auto array2_pf = &pf_array1;
    /* 
     * array2_pf指向数组,那么*array2_pf就是数组中的元素,也就是函数指针;
     * 那么(*array2_pf)[x]就是某函数指针
     * 所以,调用就是(*(*array2_pf)[x])(p, 3);
     * 最后,由于函数返回值是指针,取值要再加一个*
     */
    double a = *(*(*array2_pf)[x])(p, 3);
    

    小总结:

    指针函数一个*就可以进行调用函数;

    指针函数数组一个*可以调用指针函数,两个*可以调用函数;

    函数指针数组一个*可以调用数组,两个*可以调用指针函数,三个*可以调用函数

    还是用auto吧!

  • 或者还有个选择:typedef

    typedef sturct{
        int x, y;
    }POINT;
    
    typedef const double * (*p_fun)(const double *, int);
    p_fun p1 = f1;
    

6. 函数探幽

  • 内联函数

    内联函数的作用是提高运行速度,它与常规函数的主要区别不在于编写方式,而在于C++编译器如何将它们组合到程序中

    内联函数采用直接替代的方式,而不是通过堆栈调用,因此虽然提高效率,但是牺牲了内存,所以采用内联的函数通常较小、但被多次调用

    // 可以用来替换C中的 #define
    inline double square(double x){return x * x;}
    
  • 引用变量

    引用必须在声明时初始化(比较像const指针,从一而终)

    int &rodents = rats;
    int * const pr = &rats;
    

    如果实参与引用参数不匹配,且参数为const引用时,C++将在以下两种情况时生成临时变量

    1. 实参的类型正确,但不是左值(比如传入了x + 3、传入了3)

    2. 实参的类型不正确,但可以转换为正确的类型

      左值

      左值参数是可被引用的数据对象。如,变量、数组元素、结构成员、引用和接触引用的指针都是左值

      非左值包括字面常量(用引号括起的字符串除外,他们由地址表示)和包含多项的表达式

      常规变量和const都可以视为左值,因为可通过地址访问它们

      • 后置自增自减不能作左值,而前置可以
  • 临时变量

    这种时候会生成匿名临时变量,并让引用指向它,这些临时变量只在函数调用期间存在,最后让你函数不成功

所以现在的C++标准禁止这样创建的临时变量……(简单粗暴)

  • void swap(int &a, int &b){
        int temp = a;
        a = b;
        b = temp;
    }
    // 也就是说,下面这些调用都是错误的!(已验证)
    double a = 1, b = 2;
    swap(a, b);
    int a = 1;
    swap(a + 1, b);
    swap(a, 2);
    // 但是把swap里的int换成const int就都可以了,因为const是左值!
    
    应尽可能使用const

    这里我们再次强调const的重要性!

    1. 使用const可以避免无意中修改数据的编程错误
    2. 使用const使函数能够处理const和非const实参,否则将只能接受非const数据
    3. 使用const引用使函数能够正确生成并使用临时变量
  • 右值引用(C++11新特性)

    右值区别于上文说到的左值,那么上面的引用便称为左值引用,右值引用使用符号&&

    double &&rref = sqrt(36.00);
    int j = 15.0;
    double &&jref = 2.0 * j + 18.5;
    

    右值引用可以实现移动语义,这在之后会说到

  • 引用用于结构

    (函数返回引用有什么用?和返回值不一样吗?返回值会执行一次拷贝!地址是和原值不一样的!)

    // 另外,考虑这个情况
    POINT &accumulate(POINT &p1, const POINT &p2){
        p1.x += p2.x;
        p1.y += p2.y;
        return p1;
    }
    // 为了实现累加操作,我们必须返回引用(类似于cin >> a >> b;)
    p1 = accumulate(accumulate(p1, p2), p3);
    // 这种情况如果返回的不是引用就会报错!
    

    但是也不能因此就全部返回引用!

    // 返回引用出错了!
    int & sum(const int a, const int b){
        return a + b;
    }
    // 也就是说,不能返回右值(无法引用的对象),当然也不能返回局部变量,因为函数结束就没了
    int & sum(const int a, const int b){
        int sum = a + b;
        return sum;
    }
    
  • 对象、继承和引用

    接收基类引用作为参数的函数,调用该函数时,也可以将派生类对象作为参数**(基类引用可以指向派生类对象)**

    关于ios_base::的一点东西
    setf(ios_base::fixed);				// 将对象置于使用定点表示法的模式
    setf(ios_base::showpoint);			// 将对象置于显示小数点的模式,即使小数部分为0
    precision();						// 指定显示多少位小数(定点模式下)
    width();							// 设置下一次输出字段宽度
    
    ios_base::fmtflags initial;
    initial = os.setf(ios_base::fixed);		// 保存字体格式到initial中
    ...
    os.setf(initial);						// 重新设置为initial的字体格式
    
    什么时候使用引用参数(指导原则)
    1. 对于使用传递的值而不做修改时
      • 如果数据对象很小,如内置数据类型或者小型结构,按值传递
      • 如果数据对象是数组,则使用指针,并将指针声明为const的指针
      • 如果数据对象是较大的结构,则使用const指针或者const引用,以提高程序的效率,节省复制结构所占用的时间和空间
      • 如果数据对象是类对象的话,则使用const引用
    2. 对于修改调用函数中数据的函数
      • 如果数据对象是内置数据类型,则使用指针
      • 如果数据对象是数组,则只能使用指针
      • 如果数据对象是结构,则使用引用或者指针
      • 如果数据对象是类对象的话,则使用引用

下面是一些比较重要的类型


  • 函数重载

    参数引用和非引用无法重载

    const和非const可以重载(非const可以用const的函数)

    返回值不一样不算重载

    名称修饰

    C++如何跟踪每一个重载函数?用名称修饰(名称矫正)

    long MyFunctionFoo(int, float);

    经过编译器会转换为内部表示,来描述该接口

    ?MyFunctionFoo@@YAXH

  • 函数模板

    常将模板放在头文件中,并在需要使用模板的文件中包含头文件

    在代码中包含函数模板本身并不会生成函数定义,它只是一个用于生成函数定义的方案

    模板在函数声明上要有一个,在函数上也要有一个

    template <typename AnyType>			// 也可以用template <class AnyType>
    void swap(AnyType &a, AnyType &b){	// 更常用template <typename T>
        AnyType temp = a;
        a = b;
        b = temp;
    }
    

    重载的模板

    template <typename T>
    void swap(T &a, T &b);
    
    template <typename T>
    void swap(T *a, T *b, int a);
    

    但是模板无法应对一些问题,如结构体之间不能比较大小,数组不能赋值,指针的地址比较可能不是想要的结果等,解决方法有:① 重载运算符 ② 为特定类型提供具体化的模板定义

    模板显式具体化

    1. 对于给定的函数名,可以有非模板函数、模板函数和具体化模板函数以及它们的重载版本
    2. 显式具体化的原型和定义应以template<>打头,并通过名称来指出类型
    3. 非模板函数优先于具体化,具体化优先于常规模板
    // 非模板
    void swap(job &, job&);
    // 模板
    template <typename T>
    void swap(T &a, T &b);
    // 具体化(是在有模板的基础上规定模板的T到底是什么)
    template <> void swap<job>(job &, job &);
    
    实例化和具体化

    模板需要调用!模板需要调用!模板需要调用!

    编译器使用模板为特定类型生成函数定义时,得到的是模板实例

    1. 隐式实例化:函数调用swap(i, j),编译器通过传入类型知道怎么定义

    2. 显式实例化:直接命令编译器

      template void swap<int>(int, int);
      
    3. 显式具体化(需要自己实现函数):

      // 二者等价
      template <> void swap<job>(job &, job &);
      template <> void swap(job &, job &);
      // 函数实现
      template <> void swap<job>(job &j1, job &j2){
          double temp = j1.salary;
          j1.salary = j2.salary;
          j2.salary = temp;
      }
      

      *隐式实例化*:使用模板之前,编译器不生成模板的声明和定义示例,后面有程序用了,编译器才会根据模板生成一个实例函数
      *显式实例化*:是无论是否有程序用,编译器都会生成一个实例函数(有什么用?)
      *显示具体化*:因为对于某些特殊类型,可能不适合模板实现,需要重新定义实现,此时就是使用显示具体化的场景

    (为什么形参不能用auto自动识别呢?)

    那么编译器将选择使用哪个函数版本呢?
    1. 创建候选函数列表(包含与被调用函数的名称相同的函数和模板函数)
    2. 使用候选函数列表创建可行函数列表(都是参数数目正确的函数,不考虑返回值类型)
    3. 确定是否有最佳的可行函数(如果有,调用,否则出错)
    4. 接下来,按下面顺序匹配
      • 完全匹配,但常规函数优先于模板
      • 提升转换(char和short自动转换为int,float自动转换为double)
      • 标准转换(int转换为char,long转换为double)
      • 用户定义的转换,如类声明中定义的转换

    完全匹配和最佳匹配

    表 完全匹配允许的无关紧要转换

    从实参到形参
    TypeType&
    Type&Type
    Type[]* Type
    Type(argument-list)Type(*)(argument-list)
    Typeconst Type
    Typevolatile Type
    Type *const Type
    Type *volatile Type *

    如果有多个匹配的原因,编译器将无法完成重载解析过程

    但也有例外

    1. 指向非const数据的指针和引用优先与非const指针和引用参数匹配
    2. 如果两个完全匹配的函数都是模板函数,则较具体的模板函数优先(显式具体化优于隐式生成),“具体”指执行的转换最少
    3. 可以自己指定,如less<>(m, n);表明一定用的模板函数
    4. 当函数有多个参数时会很复杂,不讨论
  • 模板函数的发展

    decltype(x) y;C++11引入的关键字,让y的类型和x一样(x可以是表达式)

    template <class T1. class T2>
    // 由于不知道T1,T2类型,返回值类型也不好确定;也不能用decltype,因为这在x,y作用域外
    // 因此可以后置返回类型
    auto gt(T1 x, T2 y) -> decltype(x + y){
        return x + y;
    }
    

7. 内存模型和名称空间

  • 单独编译(翻译单元,translation unit)

    只修改了一个文件的话,只需要重新编译该文件,并将它和其他文件的编译版本链接(UNIX和Linux都具有make程序)

    不要将函数定义或变量声明放在头文件中

    头文件常含内容
    1. 函数原型(声明)
    2. 使用#define或const定义的符号常量
    3. 结构声明
    4. 类声明
    5. 模板声明
    6. 内联函数(inline)
    // 防卫措施
    #ifndef HEAD
    #define HEAD
    ...
    #endif
    

    不同编译器可能采用不同名称修饰(把函数名变为?MyFunctionFoo@@YAXH等),因此链接不同库文件时可能会出错,这时如果有源代码的话,需要重新编译

  • 存储持续性、作用域、链接性

    1. 存储持续性

      静态变量3种链接性:外部链接性(可在其他文件中访问)、内部链接性(只能在当前文件中访问)、无链接性(只能在当前函数或代码块中访问)

      // 未被初始化的静态变量所有位都被设置为0
      int global = 1000;				// 外部链接性
      static int one_file = 50;		// 内部链接性
      
      void func(){
          static int count;			// 无链接性
      }
      
      • (尽量不用全局变量),如果要从其他文件调用,需要extern声明

        在全局变量和局部变量同名的情况下,用::加上变量名可以调用全局变量

      • static内部链接性变量会隐藏外部链接性变量

      • static局部变量只会进行一次初始化

    2. 说明符和限定符

      • cv限定符:const、volatile(不稳定的)

      • 存储说明符:auto、register、static、extern、thread_local、mutable

        volatile表明:即使程序代码没有对内存单元进行修改,其值也可能发生变化(如,可以将一个指针指向某个硬件位置,其中包含了来自串行端口的时间或信息,这种情况下硬件可能修改其中的内容)

        mutable指出:即使结构(或类)变量为const,其某个成员也可以被修改

        struct data{
            char name[30];
            mutable int accesses;
        };
        
        const data veep = {"asd", 0};
        strcpy(veep.name, "qwe");			// 不允许
        veep.accesses++;					// 允许
        

        const特性:const全局变量和使用了static说明符是一样的,会限定为内部链接;但如果希望链接性为外部,可以用extern来覆盖:extern const int states = 50;单个const在多文件内共享,因此只有一个文件可对其进行初始化

    3. 函数和链接性

      所有函数存储持续性都自动为静态,链接性默认为外部

      可以使用extern来指出函数是在另一个文件中定义的;用static将函数的链接性设置为内部,必须在原型和定义中均使用改关键字**(静态函数覆盖外部定义)**

      内联函数不受这种限制,可以在多个文件中定义,但是定义必须相同

    4. 语言链接性

      C语言中,一个名称对应一个函数;而C++中函数有重载,通常用名称修饰

      如果要用C库中预编译的函数,可以用函数原型来指出:extern "C" void spiff(int);

    5. 存储方案和动态分配

      动态内存由new和delete控制

      如果要初始化常规结构或者数组,可以使用大括号

      struct point{
          int x;
          int y;
      };
      // C++11特性
      point* one = new point{1, 2};
      int* arr = new int[4]{1, 2, 3, 4};
      

      new失败时,将引发异常std::bal_alloc

      // 运算符new和new[]分别调用如下函数
      void* operator new(std::size_t);			// new
      void* operator new[](std::size_t);			// new[]
      // delete和delete[]同理
      void* operator delete(std::size_t);			// delete
      void* operator delete[](std::size_t);		// delete[]
      
      侯捷的课

      new:先分配memory,再调用构造函数

      Complex* pc = new Complex(1, 2);
      // 编译器大多数会像下面这样转化
      Complex* pc;
      // 是一个函数,分配内存,其内部调用malloc(n)
      void* mem = operator new(sizeof(Complex));
      // 转型(void*转型为我们需要的)
      pc = static_cast<Complex*>(mem);
      // 构造函数
      pc->Complex::Complex(1, 2);
      

      delete:先调用析构函数,再释放memory

      delete ps;
      
      // 析构函数
      String::~String(ps);
      // 释放内存,内部调用free(ps)
      operator delete(ps);
      

      定位new运算符

      指定要使用的位置。可以通过这种特性设置其内存管理规程、处理需要通过特定地址进行访问的硬件、在特定位置创建对象

      #include<new>
      struct chaff{
          char dross[20];
          int slag;
      };
      char buffer1[50];				// *之前的知识点,cout的时候要转换为别的指针,用char会直接
      char buffer2[100];				// *输出字符串来
      p1 = new (buffer1) chaff;		// 从buffer1中分配空间给chaff
      p2 = new (buffer2) int[20];		// 从buffer2中分配空间给一个包含20个元素的数组
      

      注意:定位new出的空间不能delete,因为不在堆中

      定位new运算符的另一种用法时,将其与初始化结合使用,从而将信息放在特定的硬件地址处

  • 名称空间

    // 在名称空间Jack中加入以下变量(若没有则创建)
    namespace Jack{
        double pail;
        int pal;
        struct Well {...};
    }
    

    名称空间不能位于代码块中

    ::pail调用全局的pail而不是Jack中的

    在using namespace(using声明)中,可以有隐藏变量,局部变量隐藏空间变量;而using Jack::pal(using编译指令)中,无法隐藏变量

    名称空间可以嵌套

    namespace elements{
        namespace fire{
            int flame;
        }
        float water;
    }
    // 还可以起别名
    namespace EF = elements::fire;
    // 未命名名称空间,相当于static全局变量(单文件内可用)
    namespace {
        int ice;
        int handycoot;
    }
    
    名称空间指导原则
    1. 使用已命名的名称空间中声明的变量,而不是外部全局变量
    2. 使用已命名的名称空间中声明的变量,而不是静态全局变量
    3. 如果开发了一个函数库或类库,将其放在一个名称空间中
    4. 仅将编译指令using作为一种将旧代码转换为使用名称空间的权宜之计
    5. 不要在头文件中使用using编译指令
    6. 导入名称时,首选使用作用域解析运算符或using声明的方法
    7. 对于using声明,首选将其作用域设置为局部而不是全局
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值