变量和基本类型
基本内置类型
关于 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_cast
const_cast
reinterpret_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
函数的要求
静态成员:
- 通常情况下,不应该在类的内部初始化
- 静态成员(和指针成员)可以是不完整类型,而其他的数据成员必须是完整类型
- 静态成员可以作为默认参数