变量和基本类型
基本内置类型
关于 char、signed char 和 unsigned char:
char被当成有符号或是无符号视不同编译器决定- 在定义数值类型时使用
signed char和unsigned char,signed char表示 -128 到 127,unsigned char表示 0 到 255
类型转换:
- 浮点数赋值给整数时,截断小数部分
- 整数赋值给浮点数时,小数部分记为 0,如果整数所占空间超过浮点类型的容量,精度可能有损失
含有无符号类型的表达式:
如果算术表达式中既含有无符号数又有 int,那 int 会转换成无符号数,如:
unsigned u = 10;
int i = -42;
std::cout << i + i << std::endl; // -84
std::cout << u + i << std::endl; // 4294967264
字面值:
- 整型:十进制、八进制(
024)、十六进制(0x14) - 浮点型:
3.1415、3.1415e10、0.、.01 - 字符和字符串:转义
可以添加前后缀来指定字面值的类型:
- 宽字符
L'a' - utf-8 字符串
u8"Hi!" - 整型:
42ull、42u、32ull - 单精度浮点:
1e-3f long double:3e-10L
如果使用列表初始化时,存在信息丢失的风险,那么编译器会报错
复合类型
引用、指针
注意指针解引用后是引用类型
从右向左阅读,如 int *&r 是对 int * 的引用
NULL 与 nullptr:
NULL是个预编译宏,nullptr是个编译期常量NULL的类型是void *(C)或者整型(C++),nullptr的类型是nullptr_t- 模板和函数重载时,
NULL可以匹配为整型,而nullptr不会
常量
- 常量引用
- 常量指针与指向常量的指针(
const放在*后面) - 底层
const与顶层const:本身不能改变的是顶层,本身可以改变但是指向的值不能改变的是底层,引用是底层const,因为引用本来就是不能改变的 - 底层和顶层不止可以说指针,任何变量都可以这么讲
其实可以这么区分:* 左侧是底层,右侧是顶层
const int x; // 顶层
const int * x; // 底层
int * const x; // 顶层
const int * const x; // 顶层 + 底层
const int& r; // 底层
const int&& r2; // 底层
constexpr 与常量表达式
- 可以将变量声明为
constexpr来让编译器验证该变量的值是否是常量表达式 constexpr还可以声明函数- 声明为
constexpr时使用的类型必须为字面值 constexpr可以修饰引用,此时不能指向函数内的变量constexpr声明的指针不分底层和顶层,只对指针有效,与指针指向的对象无关(只有顶层),constexpr指针可以指向常量也可以指向非常量
字面值类型:
- 算术类型、引用和指针都属于字面值类型
string类等不属于字面值类型
typedef 与常量指针
#include <iostream>
using namespace std;
typedef char* pstring;
int main() {
pstring s1;
const pstring s2 = "323";
return 0;
}
上面的代码中,s1 是 char * 类型,s2 是 char * const 类型,其中 const 修饰的是指针,它是个顶层 const
auto 类型说明符
auto修饰变量时必须有初始值,如果一条声明语句声明多个变量,那么它们的类型必须相同auto推断出的类型和初始值的类型并不完全一样,有一些规则:- 当引用被当做初始值时,使用引用对象的类型
- 一般会忽略顶层
const,同时底层const会保留下来,如果需要保留顶层const,那么需要添加const修饰符 - 在引用时同样
可以说,auto 在推断类型时总是退化的(左值到右值、数组到指针、函数到函数指针,并且去除 cv 限定符)
int i = 0, &r = i;
auto a = r; // int
const int ci = i, &cr = ci;
auto b = ci; // int
auto c = cr; // int
auto d = &i; // int *
auto e = &ci; // const int *
const auto f = ci; // const int
auto &g = ci; // const int &
auto &h = 42; // error
const auto &j = 42; // const int &
decltype 类型指示符
选择并返回操作数的数据类型,分析表达式并得到类型,但是并不实际计算表达式的值
decltype 处理引用和顶层 const 的方法与 auto 不同,它直接返回引用和 const
注意,引用从来作为其所指对象的同义词出现,只有在 decltype 处是个例外
加不加括号也会影响 decltype 的类型:
- 不加括号表示变量
- 加括号表示表达式,由于变量是可以作为左值的特殊表达式,所以会返回引用
const int ci = 0, &cj = ci;
decltype(ci) x = 0; // const int
decltype(cj) y = x; // const int &
int i = 42, *p = &i, &r = i;
decltype(r + 0) b; // int
decltype(*p) c; // int &,没有初始化
decltype((i)) d; // int &,没有初始化
decltype(i) e; // int
表达式
左值与右值
C++ 的表达式要不然是右值(rvalue),要不然就是左值(lvalue)
当一个对象被用作右值时,用的是对象的值(内容);当被用作左值时,用的是对象的身份(在内存中的位置)
一个基本原则(有一个例外):在需要右值的地方可以用左值替代,反过来不行
运算符使用和返回的值类型:
- 赋值运算符:被赋值的必须是左值,结果是左值
- 取地址符:需要左值,结果是右值
- 内置解引用运算符、下标运算符、迭代器解引用运算符、很多容器的下标运算符:结果是左值
- 内置类型和迭代器的递增递减运算符:需要左值,前置版本结果是左值,后置版本结果是右值
使用 decltype 时,左值和右值也有所不同:
- 如果表达式的求值结果是左值,那么会返回引用
- 如果是个纯右值表达式,则返回非引用
运算符与运算顺序
int i = 0;
cout << i << " " << ++i << endl;
上面的代码中,是个未定义行为
位求反运算符:反引号,所有位都求反
sizeof 运算符
- 指针:返回指针本身大小
- 引用:返回被引用对象大小
- 解引用指针:指针指向对象所占空间大小,指针不需要有效
- 数组:整个数组所占空间大小,等价于对数组中所有元素各执行一次
sizeof并求和 string对象或vector:固定部分的大小,不会计算对象中的元素占用了多少空间
类型的隐式转换和显式转换
算术转换:
- 整型提升
- 有无符号类型:
- 如果两个都是有符号或者都是无符号,那么选大的
- 如果一个有符号,一个无符号,且无符号的位数不小于有符号,那么选择有符号
- 如果一个有符号,一个无符号,且无符号的位数小于有符号,那么结果依赖于机器(?)
还有其他类型的隐式转换:
- 数组转换成指针
- 指针可以转换成
const void * - 在
if或while中转换成bool类型 - 转换成常量
- 类的单参数构造函数
显式转换:
static_castconst_castreinterpret_cast:非常危险
语句
switch 语句内部的变量定义:
如果在某处一个带有初值的变量位于作用域之外,在另一处该变量位于作用域之内,则从前一处跳转到后一处的行为是非法行为
case true:
string file_name; // 错误,控制流绕过来一个隐式初始化的变量
int ival = 0; // 错误,控制流绕过来一个显式初始化的变量
int jval; // 正确,它没有被初始化
break;
case false:
jval = next_num(); // 正确
if (file_name.empty()) { ... } // 如果绕过了上一个 case,那么这个变量就没有被初始化,造成缺陷
我们可以嵌套一个大括号来嵌套一个作用域:
case true:
{
...
}
break;
函数
函数声明、形参、实参、值传递、引用传递
不允许拷贝数组
数组形参:
int * arr[10]表示 10 个指针构成的数组int (* arr)[10]表示指向含有 10 个整数的数组的指针,用来表示多维数组int arr[][10]与上面等价
initializer_list 形参
省略符形参(C 语言中的东西):
void foo(parm_list, ...);
void foo(...);
如果函数返回值是引用类型,那么它表面函数返回一个左值
返回指针数组时的声明:
int (* func(int i))[10];
它表示输入是 int,返回一个第二维大小为 10 的二维 int 数组
或者使用后置类型,写成:
auto func(int i) -> int(*)[10];
如果我们有一个这种类型的数组的话,可以使用 decltype
默认实参
- 作用域内一个形参只能被赋予一次默认实参
- 函数的后续声明只能为之前那些没有默认值的形参添加默认实参,并且该形参右侧所有形参都必须有默认值
string screen(sz, sz, char = ' ');
string screen(sz, sz, char = '*'); // 错误:重复声明
string screen(sz = 24, sz = 80, char); // 正确:添加默认实参
局部变量不能作为默认实参:
int main() {
int h = 10;
string screen(int x, int y = h); // 错误,使用了局部变量 h
}
内联函数和 constexpr 函数
- 返回类型和形参的类型都得是字面值类型
- 函数体中的语句在运行时不执行任何操作
- 调用时,当参数都是
constexpr时返回值就是constexpr
内联函数和 constexpr 函数都是内部链接性
调试
assert 宏
NDEBUG 宏:是否处于 debug 模式
__func__ 宏:当前调试的函数名称
__FILE__、__LINE__、__TIME__、__DATE__
函数匹配
确定候选函数:
- 与被调用的函数同名
- 声明在调用点可见
从候选函数中确定可行函数:
- 形参数量与实参数量相同(默认实参除外)
- 形参类型与实参类型相同,或者实参类型能够转换成形参的类型
寻找最佳匹配:
实参类型与形参类型越接近,它们匹配地越好。对于多个形参,如果有且仅有一个函数满足条件:
- 该函数每个实参的匹配都不劣于其他可行函数需要的匹配
- 至少有一个实参的匹配优于其他可行函数提供的匹配
那么选择它,否则出现二义性
为了确定最佳匹配,编译器划分了几个等级:
- 精确匹配:
- 实参类型与形参类型相同
- 实参从数组类型或函数类型转换成对应的指针类型(也就是
int * a和int a[]等价) - 向实参中添加顶层
const或从实参中删除顶层const(例如func(const int x)可以被int x = 10; func(x)匹配)
- 通过
const转换实现的匹配(这里指的是底层const,例如const int *与int *) - 通过类型提升实现的匹配
- 通过算术类型转换(除去类型提升之外的算术类型转换)或指针转换(任何指针都可以转换成
const void *,有继承关系的类之间指针的转换)实现的匹配 - 通过类类型转换实现的匹配
函数指针
函数指针只需要将函数声明中函数名前面加上 * 并加上括号即可
在调用时可以加 * 也可以不加
例如:
int (* func(int i))[10] {
cout << "func(int i)[10]" << endl;
return nullptr;
}
int main() {
int (* (* pfunc)(int i))[10] = func;
pfunc(10);
(*pfunc)(10);
return 0;
}
当使用重载函数时,必须精确的界定使用哪个函数:
void ff(int *);
void ff(unsigned int);
void (* pf1)(unsigned int) = ff; // 正确
void (* pf2)(int) = ff; // 错误,不能匹配到任何一个函数
double (* pf3)(int *) = ff; // 错误,返回值不匹配
函数指针和函数类型可以作为形参,但是它们等价,都表示一个函数指针:
void useBigger(bool pf(const string &, const string &));
void useBigger(bool (* pf)(const string &, const string &));
上述两个函数等价
函数指针也可以作为返回值(函数类型不行):
using F = int(int *, int);
using PF = int(*)(int *, int);
PF f1(int); // 正确
F f1(int); // 错误,函数类型不能作为返回值
F * f1(int); // 正确
int (* f1(int))(int *, int); // 与上面等价
类
友元
友元不具有传递性,即如果 B 是 A 的友元,C 是 B 的友元,不代表 C 是 A 的友元
如果一个类想把一组重载函数声明成它的友元,那么它需要对这组函数中的每一个分别声明
名字查找与类的作用域
类的定义分为两阶段来处理:
- 处理成员的声明
- 在类全部可见后才编译函数体
这样的处理方式使得类成员可以使用类的定义中的任何名字,也带来了限制
如果类的成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字,如:
typedef double Money;
class Account {
public:
Money balance() { ... } // 使用了外层的 Money
private:
typedef double Money;
Money bal; // 错误:不能重新定义 Money
};
注意即使两个 Money 类型一致,这也是错误的
构造函数
构造函数初始化列表中的成员的初始化顺序:与类定义的出现顺序一致
下面的写法会导致缺陷:
class X {
int i;
int j;
public:
X(int val) : j(val), i(val) { ... } // i 在 j 之前被初始化
};
最好让初始化成员列表的顺序与类成员顺序保持一致
假如有委托构造函数,那么先执行委托构造函数的初始化,然后控制权才交给原来的构造函数
注意默认构造函数使用时:
ClassName obj1(); // 错误,它定义了一个函数
ClassName obj2; // 正确
转换构造函数只允许隐式转换一步
可以使用 static_cast 来代替显式的转换构造函数调用
字面值常量类:
- 数据成员必须都是字面值类型
- 类必须至少含有一个
constexpr构造函数 - 如果一个数据成员含有类内初始值,那么内置类型成员的初始值必须是一条常量表达式
- 如果数据成员属于某种类类型,那么初始值必须使用成员自己的
constexpr构造函数 - 类必须使用析构函数的默认定义
constexpr 构造函数必须既符合构造函数的要求,又符合 constexpr 函数的要求
静态成员:
- 通常情况下,不应该在类的内部初始化
- 静态成员(和指针成员)可以是不完整类型,而其他的数据成员必须是完整类型
- 静态成员可以作为默认参数