变量
声明与定义
默认初始化:在函数体外定义的“内置类型” 与 在任意位置定义的“支持显式初始化的类对象”会被默认初始化
声明只是使得这个变量名为程序所知,定义会向内存申请存储空间,可能会赋予初值
仅声明而不定义:加extern关键字
但若包含了显示初始化则成为定义:
extern double pi = 3.1416;
在函数体内初始化一个由extern标记的变量将报错
变量只能定义一次,但是可以声明多次
标记extern的变量的内部行为:
编译阶段:将该变量解析为一个未解析符号,放在.o或.obj里,待 链接器 来处理
链接阶段:在其他源文件中查找同名全局变量的定义,找到后对extern变量做一个地址绑定,将实际变量的绝对地址与其绑定
复合类型
指针
任何类型指针都可以直接赋给void* ,反之不能,需要用static_cast进行转换
int不能隐式转换为指针类型,0除外
const
初始化必须要赋值,且是编译期可确定的常量
在编译过程中,所有用到const变量的地方都会被替换成对应值,所以每一个用到该变量的文件都必须得能访问到它的初始值才行(毕竟链接在编译之后)
为了支持上面的用法,同时保证程序不出现const对象的重复定义问题,默认状态下,const对象仅在文件内有效,即仅内部链接
若希望实现多文件共享一个值为 运行时表达式 的const对象,则需要在定义和声明处都加一个extern修饰
// test.cpp #include "test.h" #include <iostream> extern const int bufSize = getBufSize(); void printSize() { std::cout << "Global bufSize: " << bufSize << std::endl; } int getBufSize() { // ... } // test.h #ifndef TEST_H #define TEST_H extern const int bufSize; void printSize(); int getBufSize(); #endif //TEST_H // main.cpp #include <iostream> #include "test.h" int main() { std::cout << "Global bufSize from File2: " << bufSize << std::endl; printSize(); return 0; }
const的引用
对const对象的引用也必须是const的
不能对const的引用赋值
const int ci = 32; const int& r1 = ci; // fault: // r1 = 42; // int& r2 = ci;
引用的初始化一般都要求对象与引用的类型一致,但是存在例外
第一种例外:const引用允许用任意表达式作为初始值,只要该初始值能隐式转换为引用类型即可
double i = 42; const int& r1 = i * 2.0;
内部原理:
double i = 42; // 这也是为什么普通引用不支持这种特性 const int temp = i * 2.0; const int& r1 = temp;
指针和const
(tips: 从右往左读)
指向const的指针:
const int* ptrconst指针:
int* const ptr二者组合:
const int* const ptr指向const的指针是底层const,可以改变指针指向但是不能通过指针改变对象的值
底层const ⇒ 顶层const
类型必须一致(const-ness可以不一致)
const int i1 = 5; int i2 = 10; const int* cptr = &i2; cptr = &i1; // fault: // (*cptr)++;
constexpr
常量表达式:值不会改变并且在编译期就能获得结果的表达式,包括字面值和用常量表达式初始化的const对象
将变量声明为constexpr,由编译器去验证变量的值是否为常量表达式
字面值类型:可以用字面值初始化的
constexpr inline int get_size(int x) { return 2 * x; } // 如果get_size()前面没有加constexpr则报错: // Constexpr variable 'size' must be initialized by a constant expression constexpr int size = get_size(2);
注:数组定义时的元素个数也得是constexpr
constexpr函数:能用于常量表达式的函数
C++11标准中,函数返回值类型必须是字面值类型,函数体内有且仅有一条return语句,其他语句在运行时不能执行任何操作,比如空语句、类型别名和using声明
C++14之后允许在函数体内有一些执行操作的语句,比如声明变量和循环语句
constexpr指针 V.S. 指向const的指针
// p1是常量 constexpr int* p1 = nullptr; // p2指向的对象是常量 const int* p2 = nullptr; // p3是常量 int* const p3 = nullptr;
static
标记某个变量,其存储位置不会发生改变(始终在静态变量区),生命周期与进程相同
如果标记一个全局变量/方法,则其作用域会限制于当前文件
处理类型
typedef, using
// pbase 等价于 double*, base 等价于 double typedef double base, *pbase; using SI = SalesItem; SI item;
with指针和常量:
注意不要直接文字替换「const 指针别名类型」
typedef char* pstring; // 等价于char* const const pstring cstr = nullptr; // ps不是const,ps指向的pstring对象是const(即指向char的常量指针(char* const)) const pstring* ps;
类型说明符auto
一条auto声明语句的所有变量的初始基本类型必须一致
auto ch = 'a', pi = 3.14 // fault
with复合类型、常量
声明变量前没有&的情况下,使用引用作为初值则auto会将其推断为引用对象的类型,而非引用类型
int i = 0, &r = i; auto a = r; // a是int,不是int& auto &ra = r; // ra是int&
对于指针的情况,auto会忽视顶层const(如果不是指针则自己的const就是顶层const),底层const会被保留
e.g. 指向常量的指针会保留底层const,常量指针不会保留顶层const
这个特性很好理解,想保留顶层const那就直接在auto前面加const修饰即可
int i = 0; const int ci = i, &cr = ci; auto b = ci; //b是一个整数(ci的顶层 const 特性被忽略掉了) auto c = cr; //c是一个整数(cr是ci的别名,ci本身是一个顶层 const) auto d = &i; //d是一个整型指针(整数的地址就是指向整数的指针) auto e = &ci; //e是一个指向整数常量的指针(对常量对象取地址是一种底层 const)
复合声明:
每一行的基础类型得是一样的,包括const-ness

类型说明符decltype
作用于任意表达式(主要是函数)以获取其返回值类型
decltype处理表达式的时候会保留顶层const和引用
decltype对于解引用操作表达式会返回引用(毕竟解引用之后可以直接对源对象进行操作,可以认为是一种引用)
变量名(任何形式的复杂表达式不算) + 一层括号 会自动转换成引用
int get_size(int t) { return sizeof(t); } const int ci = 0, &cj = ci; decltype(ci) x = 0; // const int // x = 1; fault decltype(cj) y = x; // const int& int i = 42, *p = &i, &r = i; decltype (r + 0) b; // 加法的结果是 int,因此b是一个(未初始化的) int decltype (*p) с; // fault: c是int&,必须初始化 decltype((i)) d; // fault:d 是 int&,必须初始化 decltype((get_size())) f; // f 是 int
补充:引用的底层机制
C++标准没有规定引用是否需要占用额外空间
根据编译器不同和场景会有不同表现
当作为一个类成员时,会以指针的方式实现,只是该指针添加了“非空”和“不可变绑定”机制
在局部块里可能会直接被编译器优化成一个常量