一、基本类型(内置)

整型 整型 整型 short
int
long
long long
字符 char
wchar_t
char16_t
char32_t
布尔值 bool
浮点型 float
double
long double
void类型

C++标准规定的各类型最小尺寸:
short <= int <= long <= long long

类型 最小尺寸 含义
bool 未定义 布尔类型
char 8bit 字符
wchar 16bit 宽字符
char16_t 16bit Unicode字符
char32_t 32bit Unicode字符
short 16bit 短整型
int 16bit 整型l
long 32bit 长整型
long long
C++11定义
64bit 长整型
float 6位有效数字 单精度浮点数
double 10位有效数字 双精度浮点数
long double 10位有效数字 扩展精度浮点数

内置类型机器实现

可寻址的最小内存块称为“字节(byte)”,存储的基本单元称为“字(word)”。
以下是字节为8bit,字为32bit的机器上一个字的内存区域:

  • 736424为这个字的地址,736425为这个字的第2个字节的地址。
  • 这个字的内存如何被解释,取决于它的数据类型。

image.png

带符号和无符号

  1. signed int a; // 有符号,signed + 类型
  2. unsigned int a; // 无符号,unsigned + 类型
  3. char a; // 一般解释为signed char
  4. float a; // double的性能float和差不多,但精度更高。
  5. double a;
  6. // 关于有符号类型的取值范围,C++并为规定,只规定了正负数必须平衡。
  7. signed char a; // 取值范围:-128 ~ 127
  8. unsigned char a; // 取值范围:0 ~ 255
  9. int a; // -2147483648 ~ -2147483647
  10. unsigned int a; // 0 ~ 4294967295

类型转换

算术->bool,0为false,其他为true。
bool->算术,false为0,true为1。
浮点->整型,保留整数部分。
整型->浮点,小数部分标记为0,如果整型空间大于浮点,损失精度。
无符号超出范围,模除取余。
带符号超出范围,结果不可以预知。

要避免不可预知和依赖于环境的行为,如把int看成就是4byte,这样的程序是不可移植的。

  1. // 无符号尽量避免用于循环。
  2. for (unsigned u = 10; u >= 0 ; --u ) //死循环
  3. std :: cout << u << std: : endl ;
  4. // 有符号和无符号不要混用
  5. int a = 1, b = -1;
  6. unsigned int c = 1;
  7. cout << a * b << endl; // -1
  8. cout << a * c << endl; // 42亿多

字面值常量literal

整型
十进制、八进制(0开头)、十六进制(0x、0X开头)。
十进制数的类型是有符号的int~long long之间最小的能装下它的,都不能装下,报错。
八、十六进制的类型是无论有符号的int~long long之间最小的能装下它的,都不能装下,报错。

浮点型
3.141593 .14159E0 0. 0e0 .001

字符(串)
‘ a ‘ :字符字面值
“ Hello World!”:字符串字面值,实质是\0结尾的字符数组。

转义序列
\ + 1~3为八进制数字,最多能转到3位,超过的另算,如\1234是\123和4两个字符。
\x + 任意位十六进制数字,根据(扩展)字符集转义结果,可能会转义不出来报错。

换行符 \n(\12) 横向制表符 \t
纵向制表符 \v 退格符 \b
反斜线 \\ 问号 \?
回车符 \r 进纸符 \f
报警( 响铃)符 \a(\7) 双引号 \“
\40 (空格) 单引号 \‘

指定字面值

整型字面值 浮点型
后缀 最小匹配 后缀 最小匹配
u 或 U unsigned f 或 F float
l 或者 L long l 或 L long double
ll 或 LL long long
字符(串)字面值
前缀 含义 类型
u Unicode16字符 char16_t
U Unicode32字符 char32_t
L 宽字符 wchar_t
u8 utf8仅限字符串 char

布尔字面值
true、false

二、变量

有名字,有类型的一块内存空间。类型决定内存空间大小等。

列表初始化、默认初始化

用花括号初始化叫列表初始化,如果存在信息丢失,会编译报错,多使用这种

内置类型定义在函数外部默认初始化为0,否则是未初始化。在函数外部的变量其实在编译阶段已经在操作它了,也就是说程序加载到内存的时候,它就已经在内存当中了;而函数内部的变量,只有在执行函数压栈了才会加载到内存,这两个是完全不一样的生命周期。

  1. int a = 1, b = 2; // 正确
  2. int a(0); // 正确
  3. int a, b = 1, 2; // 编译报错
  4. int a, b = 1; // a未初始化,b初始化1
  5. int units_sold = {0}; // 列表初始化
  6. int units_sold{0} ; // 列表初始化
  7. long double ld = 3.1415926536;
  8. int a{ld}, b = {ld} ; // 编译报错:转换未执行,因为存在丢失信息的危险
  9. int c(ld), d = ld; // 正确: 转换执行,且确实丢失了部分值

声明、定义

基本数据类型 声明符, 声明符,…声明符;

声明:我要使用一个来自外部的变量。这样编译器就会知道你要用的这个变量来自文件外。它就会去其他文件查找,然后把你们“链接”起来,你就能通过这个变量访问到它的内存地址了。
定义:我要创建一个自己的变量。编译器就会知道这个变量是你这里的,别人如果要用,编译器也能从你这里找到。
最终的目的就是,编译器可以单个编译程序文件(程序代码),通过“链接”组装起来就称为一个(程序)可执行文件。

  1. extern int i; // 声明i 而非定义i
  2. int j; // 声明并定义j
  3. extern int i = 1; // 定义i,而非声明i
  4. int fuck(){
  5. extern int i = 1;
  6. // 编译器:“你特么到底几个意思,既然extern声明,你就干脆在函数外进行
  7. // 就算你是声明,你初始化是干啥,到底是声明还是定义
  8. // 我不干了,我报错”
  9. }

C++是静态类型语言, 即使在编译阶段确定变量的类型(类型检查),类型决定了一个变量的内存“含义”。

名字(标识符)

变量(类、对象)、函数、类型都可以有自己的名字。
C++内部用了的叫保留字(关键字)。

  1. int __a = 1; // 错误:不要两个下划线开头
  2. int _A = 1; // 错误:不要下划线接大写字母开头
  3. int _a = 1; // 错误:外部变量不要下划线卡头

作用域

  • 全局作用域
  • 局部作用域
  • 语句作用域
  • 类作用域
  • 命名空间作用域
  • 文件作用域

一块“限制影响”的区域,比如你在当前作用域做了一些动作(定义变量、函数、类型),那么这些动作的影响范围就是当前位置~作用域结束的位置。
块作用域:{}花括号括起来的。
全局作用域:块作用域之外的。
作用域可以嵌套,分内层作用域、外层作用域。

  1. #include <iostream>
  2. int main () { // 名字main表示一个函数,在全局作用域,所有文件都可以使用
  3. // 全局作用域:花括号之外
  4. { // 块作用域:我们设为a
  5. int sum = 0; // 局部变量sum,只能在作用域a中存在。
  6. for (int val = l ; val <= 10 ; ++val)
  7. sum += val;
  8. std::cout << "Sum of 1 to 10 inclusive is" << sum << std: : endl;
  9. } // 作用域a结束
  10. return 0;
  11. }

三、复合类型(引用、指针)

引用
变量的另外一个名字,第一个名字是定义的时候,定义时必须初始化。
左值引用、右值引用。

  1. int i1 = 1, i2 = 2;
  2. int &ref1 = i1, &ref2 = i2; // 引用
  3. int &ref1, &ref2; // 错误,必须初始化
  4. ref1 = 2; // i1 = 2
  5. int i3 = ref; // i3 = 1

指针
是一个变量(一块内存),存储指定类型对象的内存地址值。定义时,请初始化为nullptr或者其他合法地址。

void*指针,保存任何类型对象的内存地址,无法解引用,因为不知道如何解释内存值。

  1. int i = 1, *p1, *p2;
  2. p1 = &i; // p1的值 = i变量的内存地址。
  3. int* p3 = &i; // 定义并初始化p3,注意要用取地址符(&)
  4. double d = 1.0;
  5. int zero = 0;
  6. int* p1 = &d; // 错误,类型必须严格匹配。
  7. double* pd = zero; // 错误,类型不匹配,虽然是0。
  8. int* p1, p2; // 避免这种定义声明方式,容易歧义
  9. int* p1, *p2; // 指针正确的定义声明方式。
  10. cout << &p1 << endl; // 解引用符(*)
  11. int *p = 0; // 指向内存地址0(一般都是不可访问地址),所以解引用将报错。
  12. int *p = NULL; // 预处理变量,等价于int *p = 0;
  13. int *p = nullptr; // C++11引入,真正的空指针,不指向任何内存,代替NULL和0
  14. int **p2p = nullptr; // 指针的指针,和普通指针一样,只不过存储的是指针地址。
  15. p2p = &p;
  16. *p2p; // 指针p的值
  17. **p2p; // 指针p解引用,也就是*p
  18. int *&ref_p = p; // 指针的引用
  19. // 从右往左阅读
  20. // 对于复杂的语句,我们别忘记了用从右往左阅读的习惯。比如下面:
  21. // 1、ref_p2p是一个引用
  22. // 2、引用的对象是一个指针的指针
  23. // 所以紧挨名字右边的符号决定了类型。
  24. int **&ref_p2p = p2p; // 指针的指针的引用

const(read only)

const限定符,声明对一个对象的操作方式是只读模式。

  1. int i = 42;
  2. const int ci = i; // const定义声明。
  3. int j = ci; // 可以进行非修改变量的操作。
  4. const int &const_ref_ci = ci; // 正确
  5. int& ref_ci = ci; // 编译错误,必须const修饰引用。
  6. /******多文件共享const对象**********/
  7. // file1.h
  8. extern const int MOTHER_FUCKER; // 声明MOTHER_FUCKER会在其他文件用到。
  9. // file2.cc
  10. extern const int MOTHER_FUCKER = 1; // 定义
  11. int i = 42 ;
  12. const int r1 = i; // 允许将 r1 绑定到普通对象上
  13. const int &r2 = 42; // 正确 : r1 是一个常量引用
  14. const int &r3 = r1 * 2 ; // 正确 : r3 是一个常量引用
  15. int &r4 = r1 * 2; // 错误 : r4 是一个普通的非常量引用
  16. const double pi = 3.14;
  17. double *ptr = &pi ; // 错误: ptr是一个普通指针
  18. const double *cptr = &pi ; // cptr可以指向一个双精度常量
  19. *cptr = 42; // 错误: 不能给*cptr 赋值
  20. double dval = 1.0;
  21. cptr = &dval; // 正确。以只读模式来理解就顺了。
  22. int errNumb = O;
  23. int *const curErr = &errNumb ; // curErr将一直指向errNumb
  24. const double pi = 3.14159;
  25. const double *const pip = &pi; // pip是一个指向常量对象的常量指针

constexpr

常量表达式满足两个条件:

  • 编译阶段计算出结果
  • 不会改变

constexpr的需求场景,很显然在一个复杂的系统中,常量是经常被用到的,但是并没有很好的办法可以保证一个表达式是一个常量表达式。通过constexpr显式声明为常量表达式,编译器就会在编译阶段检查该表达式是否是常量表达式,否则编译错误。

  1. const int max_files = 20; // 常量表达式
  2. const int limit = max_files + 1; // 常量表达式
  3. int staff_size = 27; // 编译错误,不是常量表达式,可修改。
  4. const int size = get_size(); // 编译错误,get_size()不是constexpr函数
  1. constexpr int expr = 1; //
  2. constexpr int limit = expr + l ; // 正确
  3. constexpr int sz = size(); // size()如果是constexpr函数,则编译通过。
  4. // 注意下面const和constexpr的区别
  5. const int *p = nullptr; // p的类型是const int*
  6. constexpr int *q = nullptr ; // q的类型是int* const

constexpr声明时用的类型必须是很简单的,类型可以是下面这些:

  • 字面值类型
    • 算术类型
    • 引用
    • 指针
      • nullptr
      • 0、NULL
      • 全部变量指针
  • 字面值常量类
    • 数据成员都是字面值类型的聚合类

四、处理类型

typedef

类型也可以有别名。传统方法typedef,新标准using。

  1. /*********************语法**************************/
  2. // alter1, alter2,...,alter3 和 TypeName 是同义词
  3. typedef TypeName alter1, alter2,...,alter3
  4. /*********************例子**************************/
  5. typedef double wages; // wages是double的同义词
  6. typedef wages base,*p; // base是double的同义词, p是double*的同义词
  7. typedef char *pstring; //pstring是char*,要把char*看成一个整体的不可分割的类型了。
  8. const pstring str = "asdfasdf"; // 等价
  9. char *const str = "asdfasdf"; // 等价
  10. const char* str = "asdfasdf"; // 不等价,不能简单的替换来看。

using

头文件最好不要使用using,因为头文件会被多次包含,这样容易造成名字冲突。

  1. // 意义:使用命名空间name里定义的所有名字。
  2. // name: 命名空间,如std、std::placeholders
  3. // 尽量避免使用,造成命名空间污染。
  4. using namespace name;
  5. //例如:
  6. using namespace std;
  7. // 意义:使用某个命名空间的名字name,无需再显式加上前面的命名空间
  8. // 如:using std::placeholders::_1,使用std下的placeholders下的_1名字
  9. // 推荐使用,要啥就显示指定。
  10. using namespace_1::...::namespace_n::name;
  11. //例如:
  12. using std::cin;
  13. using std::cout;
  14. // 意义:newname是oldname的同义词
  15. // newname: 新的名字
  16. // oldname: “旧”的名字:可能是名字,类名,namespace等。
  17. using newname = oldname;
  18. //例如:
  19. using SI = Sales_item; //SI是Sales item的同义词

auto

通过初始值来推断表达式类型,编译阶段完成。这样就可以不用再写声明/定义的 变量的类型。
auto会忽略顶层const(最右),保留底层const,也就是保留对象本身的类型。如果需要保留顶层,需要显式声明const auto

  1. // 由 val1 和 val2 相加的结果可以推断出 item 的类型
  2. auto item = vall + val2; // item 初始化为 vall 和 val2 相加的结果
  3. auto i = 0 , *p = &i ; // 正确: i是整数、 p 是整型指针
  4. auto sz = 0 , pi = 3.14; // 错误 : sz 和 pi 的类型不一致
  5. int i = 0, &r = i;
  6. auto a = r; // a 是一个整数
  7. // auto 对初始值的const的处理:抛弃顶层const,暴露底层const
  8. const int ci = i, &er = ci;
  9. auto b = ci ; // b是一个整数 (ci的顶层const特性被忽略掉了)
  10. auto c = er; // c是一个整数(er是ci的别名,ci本身是一个顶层const)
  11. auto d = &i; // d是一个整型指针(整数的地址就是指向整数的指针)
  12. auto e = &ci; // e是一个指向整数常量的指针 (对常量对象取地址是一种底层const)
  13. const auto f = ci; // ci的推演类型是int, f是const int
  14. auto &g = ci; // g是一个整型常量引用,绑定到ci
  15. auto &h = 42; // 错误:不能为非常量引用绑定字面值
  16. const auto &j = 42 ; // 正确:可以为常量引用绑定字面值
  17. auto k = ci , &l = i; // k是整数, l是整型引用
  18. auto &m = ci, *p = &ci; // m是对整型常量的引用,p是指向整型常量的指针
  19. auto &n = i , *p2 = &ci; // 错误:i的类型是int而&ci的类型是const int

decltype

decltype( expr ),编译器根据表达式推断类型,而不执行表达式。假如被调用,返回的类型。
decltype对const是保留全部const(包括顶层const),和auto不太一样。

  1. decltype(f()) sum = x; //sum 的类型就是函数f的返回类型
  2. const int ci = 0 , &cj = ci ;
  3. decltype (ci) x = O; // x 的类型是 const int
  4. decltype (cj) y = x; // y 的 类型是 const int&, y 绑定到变量 x
  5. decltype (cj) z; //错误:z 是一个引用,必须初始化
  6. //decltype 的结果可以是引用类型
  7. int i = 42 , *p = &i , &r = i;
  8. decltype(r + 0) b; //正确:加法的结果是int,因此b是一个(未初始化的)int
  9. decltype(*p) c; //错误:e是int&,必须初始化
  10. //decltype的表达式如果是加上了括号的变量, 结果将是引用
  11. decltype((i)) d; //错误: d 是 int& ,必须初始化
  12. decltype (i) e; //正确: e 是一个(未初始化的)int

五、自定义数据结构(struct、类)

  1. struct Sales_data { /* . . . */ } accum, trans, *salesptr;
  2. //与上一条语句等价,但可能更好一些
  3. struct Sales data { /*. . . */ };
  4. Sales_data accum, trans , *salesptr;