Table of Contents:

const

总之就是一句话:尽可能多用 const,让代码更安全、灵活。

一般用法

const int MAXNUM = 100; 
由于常量一旦被创建后其值就不能再改变,所以常量必须在定义的同时赋值(初始化),后面的任何赋值行为都将引发错误。
By Default, const Objects Are Local to a File,在编译的时候就已经被替换了。所以作用域是当前文件。

int n = 90;
const int MaxNum1 = getNum(); //运行时初始化
const int MaxNum2 = n; //运行时初始化
const int MaxNum3 = 80//编译时初始化

C++中的const又玩出了新花样

  1. C语言对 const 的处理和普通变量一样,会到内存中读取数据,可通过指针修改; C++ 对 const 的处理更像是编译时期的#define,是一个值替换的过程。
  2. 在C语言中,const 变量和普通变量一样,在其他源文件中也是可见的. C++ 规定,全局const变量的作用域是当前文件,这和添加了static关键字的效果类似。
void main()
{
    const int a = 10;   //c里面的const是一个冒牌货
    //a = 11; 直接修改a不可以
    int *p = (int *)&a;
    *p = 11; //但是间接修改a可以 ,而在C++中却不可以
    printf("a: %d \n", a);
    system("pause");
}

const和指针,判别时用就近原则

const int *p1;     //指向常量的指针
int * const p3;    //常量指针

const 和非 const 类型转换

const char *char *是不同的类型,不能将const char *类型的数据赋值给char *类型的变量。但反过来是可以的,编译器允许将char *类型的数据赋值给const char *类型的变量。
这种限制很容易理解,char *指向的数据有读取和写入权限,而const char *指向的数据只有读取权限,降低数据的权限不会带来任何问题,但提升数据的权限就有可能发生危险。

编译器会为const引用创建临时变量

引用不能绑定到临时数据
原因有二:1.临时数据如表达式的结果、函数的返回值等可能存在寄存器中 2.常量表达式由于不包含变量,没有不稳定因素,所以在编译阶段就能求值,所以没办法法寻址,故不能用指针指向它,也不能进行引用。
当引用作为函数参数,有时候很容易给它传递临时数据,这个要注意。

bool isOdd(int &n){
    if(n%2 == 0){
        return false;
    }else{
        return true;
    }
}

int a = 100;
isOdd(a);  //正确
isOdd(a + 9);  //错误
isOdd(27);  //错误

但是当使用 const 关键字对引用加以限定后,引用就可以绑定到临时数据了。为 const 引用创建临时变量反而会使得引用更加灵活和通用.
将常引用绑定到临时数据时,编译器采取了一种妥协机制:编译器会为临时数据创建一个新的、无名的临时变量,并将临时数据放入该临时变量中,然后再将引用绑定到该临时变量。注意,临时变量也是变量,所有的变量都会被分配内存。

例子:

bool isOdd(const int &n){  //改为常引用
    if(n/2 == 0){
        return false;
    }else{
        return true;
    }
}
//由于在函数体中不会修改 n 的值,所以可以用 const 限制 n,这样一来,下面的函数调用就都是正确的了:
int a = 100;
isOdd(a);  //正确
isOdd(a + 9);  //正确
isOdd(27);  //正确
isOdd(23 + 55);  //正确

const 和 引用

不同类型的数据占用的内存数量不一样,处理方式也不一样,指针的类型要与它指向的数据的类型严格对应。

int n = 100;
int *p1 = &n;  //正确
float *p2 = &n;  //错误

引用(Reference)和指针(Pointer)在本质上是一样的,引用仅仅是对指针进行了简单的封装,类型严格一致这条规则同样也适用于引用。
但给引用添加 const 限定后,不但可以将引用绑定到临时数据,还可以将引用绑定到类型相近的数据,这使得引用更加灵活和通用,它们背后的机制都是临时变量。

const 和函数形参

const形参好处:
1. 使用 const 可以避免无意中修改数据的编程错误;
2. 清楚的分清参数的输入和输出特性
3. 可以接受const和非const的变量,否则将只能接收非 const 类型的实参
4. 使用 const 引用能够让函数正确生成并使用临时变量,可以将引用绑定到类型相近的数据,而不仅仅是严格匹配

double volume(const double &len, const double &width, const double &hei){
    return len*width*2 + len*hei*2 + width*hei*2;
}
int main(){
    int a = 12, b = 3, c = 20;
    double v1 = volume(a, b, c);
    double v2 = volume(10, 20, 30);
    double v3 = volume(89.4, 32.7, 19);
    double v4 = volume(a+12.5, b+23.4, 16.78);
    double v5 = volume(a+b, a+c, b+c);
}

const成员变量和成员函数(常成员函数)

constexpr:验证是否为常量表达式

常量表达式,是由多个(≥1)常量组成的表达式。常量表达式一旦确定,其值将无法修改。
我们知道,C++ 程序的执行过程大致要经历编译、链接、运行这 3 个阶段。值得一提的是,常量表达式和非常量表达式的计算时机不同,非常量表达式只能在程序运行阶段计算出结果;而常量表达式的计算往往发生在程序的编译阶段,这可以极大提高程序的执行效率,因为表达式只需要在编译阶段计算一次,节省了每次程序运行时都需要计算一次的时间。

对于用 C++ 编写的程序,性能往往是永恒的追求。那么在实际开发中,如何才能判定一个表达式是否为常量表达式,进而获得在编译阶段即可执行的“特权”呢?除了人为判定外,C++11 标准还提供有 constexpr 关键字。
constexpr 关键字的功能是使指定的常量表达式获得在程序编译阶段计算出结果的能力,而不必等到程序运行阶段。C++ 11 标准中,constexpr 可用于修饰普通变量、函数(包括模板函数)以及类的构造函数。

 注意,获得在编译阶段计算出结果的能力,并不代表 constexpr 修饰的表达式一定会在程序编译阶段被执行,具体的计算时机还是编译器说了算。

C++11 constexpr和const的区别

在C++ 98之前const是有两种语义的:1.只读 2.常量
C++ 11标准中,为了解决 const 关键字的双重语义问题,保留了 const 表示“只读”的语义,而将“常量”的语义划分给了新添加的 constexpr 关键字。因此 C++11 标准中,建议将 const 和 constexpr 的功能区分开,
* 即凡是表达“只读”语义的场景都使用 const,
* 表达“常量”语义的场景都使用 constexpr。

只读”和“常量”之间并没有必然的联系

int main()
{
    int a = 10;
    const int & con_b = a; //只读,但并不意味着值为常量
    cout << con_b << endl;
    a = 20;
    cout << con_b << endl;  //变量的内容变了
}

static

修饰类成员

在创建第一个对象时,所有的静态数据都会被初始化为零。
我们不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化

1)  一个类中可以有一个或多个静态成员变量,所有的对象都共享这些静态成员变量,都可以引用它。
2) static 成员变量和普通 static 变量一样,都在内存分区中的全局数据区分配内存,到程序结束时才释放。static成员变量的内存既不是在声明类时分配,也不是在创建对象时分配,而是在类外初始化时分配。反过来说,没有在类外初始化的 static 成员变量不能使用。
3) 静态成员变量必须初始化,而且只能在类体外进行。例如: int Student::m_total = 10;
初始化时可以赋初值,也可以不赋值。如果不赋值,那么会被默认初始化为 0。全局数据区的变量都有默认的初始值 0,而动态数据区(堆区、栈区)变量的默认值是不确定的,一般认为是垃圾值。
4) 静态成员变量既可以通过对象名访问,也可以通过类名访问,但要遵循 private、protected 和 public 关键字的访问权限限制。当通过对象名访问时,对于不同的对象,访问的是同一份内存。

静态成员函数与普通成员函数的区别:
- 静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)。
- 普通成员函数有 this 指针,可以访问类中的任意成员,包括静态变量和函数;

存储类定义

C++ 程序中变量/函数的范围(可见性)和生命周期。
- auto 关键字用于两种情况:声明变量时根据初始化表达式自动推断该变量的类型、声明函数时函数返回值的占位符。
- register 存储类用于定义存储在寄存器中而不是内存中的局部变量。
- extern  修饰符通常用于当有两个或多个文件共享相同的全局变量或函数的时候
- static 修饰局部变量->改变其声明周期 修饰全局变量->内部链接性,作用域限制在本文件中

volatile修饰符
对于一般的变量编译器会对变量进行优化,将内存中的变量值放在寄存器中以加快读写效率。
而volatile 告诉编译器不需要优化volatile声明的变量,让程序可以直接从内存中读取变量。

extern

在C语言中,修饰符extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。
也就是引用另一个文件的全局变量或函数要用extern进行引用声明。
若果有多个extern,最好写到头文件中。

1.修饰变量的声明

能够被其他模块以extern修饰符引用到的变量通常是全局变量
extern int v可以放在文件中的任何地方

2.修饰函数声明

从本质上来讲,变量和函数没有区别,对于函数extern可以省略。
对其他模块中函数的引用,最常用的方法是包含这些函数声明的头文件
使用extern和包含头文件来引用函数有什么区别呢?
extern的引用方式比包含头文件要简洁得多!extern的使用方法是直接了当的,想引用哪个函数就用extern声明哪个函数。这大概是KISS原则的一种体现吧!
这样做的一个明显的好处是,会加速程序的编译(确切的说是预处理)的过程,节省时间。在大型C程序编译过程中,这种差异是非常明显的。
但坏处是函数的原型该了,你不得不改掉所有其他文件中的extern的声明

3.extern “C”

目的是为了实现C++与C及其它语言的混合编程
extern "C"包含双重含义:首先,被它修饰的目标是“extern”的;其次,被它修饰的目标是“C”的。
被extern "C"修饰的变量和函数是按照C语言方式编译和链接的

比如在C++中调用C库函数,就需要在C++程序中用extern “C”声明要引用的函数。这是给链接器用的,告诉链接器在链接的时候用C函数规范来链接。主要原因是C++和C程序编译完成后在目标代码中命名规则不同
应用场合:
* 1.C++代码调用C语言代码、在C++的头文件中使用
* 2.C++调用一个C语言编写的.DLL时,当包括.DLL的头文件或声明接口函数时,应加extern "C"{}。
* 函数指针:当在C++中使用C语言的函数指针时,需要使用extern "C"来声明这些函数指针,以便正确地处理函数指针的调用和链接。

例子:

// mylibrary.c
#include <stdio.h>

int globalVar = 10;

void myCFunction(int a, int b) {
    printf("Sum: %d\n", a + b);
}

// mylibrary.h
#ifdef __cplusplus
extern "C" {
#endif

extern int globalVar;
extern void myCFunction(int a, int b);

#ifdef __cplusplus
}
#endif

//===========================
// main.cpp
#include "mylibrary.h"

int main() {
    int x = 5;
    myCFunction(x, globalVar);
    return 0;
}

inline

inline内联函数详解

函数调用是有时间和空间开销的。
程序在执行一个函数之前需要做一些准备工作,要将实参、局部变量、返回地址以及若干寄存器都压入栈中,然后才能执行函数体中的代码;
函数体中的代码执行完毕后还要清理现场,将之前压入栈中的数据都出栈,才能接着执行函数调用位置以后的代码。
由于内联函数比较短小,我们通常的做法是省略函数原型,将整个函数定义(包括函数头和函数体)放在本应该提供函数原型的地方

宏定义一不小心就会踩坑,而且不一定在编译和运行时发现,给程序埋下隐患。
和宏一样,内联函数可以定义在头文件中(不用加 static 关键字),并且头文件被多次#include后也不会引发重复定义错误。
这一点和非内联函数不同,非内联函数是禁止定义在头文件中的,它所在的头文件被多次#include后会引发重复定义错误。

inline只是一种请求,编译器不一定允许这种请求。

内联函数在编译时会将函数调用处用函数体替换,编译完成后函数就不存在了,所以在链接时不会引发重复定义错误
这一点和宏很像,宏在预处理时被展开,编译时就不存在了。从这个角度讲,内联函数更像是编译期间的宏
主要有两个作用:
* 一是消除函数调用时的开销
* 二是取代带参数的宏。不过我更倾向于后者,取代带参数的宏更能凸显内联函数存在的意义。

虚函数(virtual)可以是内联函数(inline)吗?

如何规范地使用C++内联函数

inline 是一种“用于实现的关键字”,而不是一种“用于声明的关键字”,故在函数定义处添加,在函数声明处添加 inline 关键字是无效的,编译器会忽略函数声明处的 inline 关键字。

尽管大多数教科书中在函数声明和函数定义处都增加了 inline 关键字,但我认为 inline 关键字不应该出现在函数声明处。这个细节虽然不会影响函数的功能,但是体现了高质量 C++ 程序设计风格的一个基本原则:声明与定义不可混为一谈用户没有必要、也不应该知道函数是否需要内联

更为严格地说,内联函数不应该有声明应该将函数定义放在本应该出现函数声明的地方,这是一种良好的编程风格
在多文件编程时,建议将内联函数的定义直接放在头文件中,并且禁用内联函数的声明,因为将内联函数的声明和定义分散到不同的文件中会出错

对内联函数进行任何修改,都需要重新编译函数的所有客户端,因为编译器需要重新更换一次所有的代码。

register

register关键字请求“编译器”将局部变量存储于寄存器中,以加快其存储速度。
C语言中无法取得register变量地址
C++中可以取得register变量的地址 C++编译器发现程序中需要取register变量的地址时,register对变量的声明变得无效
C++编译器有自己的优化方式,不使用register也可能做优化
早期C语言编译器不会对代码进行优化,因此register变量是一个很好的补充。然而,随着编译程序设计技术的进步,在决定那些变量应该被存到寄存器中时,现在的C编译器能比程序员做出更好的决定。实际上,许多编译程序都会忽略register修饰符,因为尽管它完全合法,但它仅仅是暗示而不是命令。

int main()
{
    register int a = 0;
    printf("&a = %x\n", &a);
    system("pause");
    return 0;
}