写在前面
简单记录一些遇到的C++语法和注意事项。
一、算法补充
- 简单记录在刷算法过程中涉及到的C++语法和注意事项;
1. 余数三大定理和常见类型范围
加法定理: ( a + b ) % c = ( a % c + b % c ) % c 加法定理:(a+b)\%c=(a\%c+b\%c)\%c 加法定理:(a+b)%c=(a%c+b%c)%c
乘法定理: ( a ∗ b ) % c = ( a % c ∗ b % c ) % c 乘法定理:(a*b)\%c=(a\%c*b\%c)\%c 乘法定理:(a∗b)%c=(a%c∗b%c)%c
若a和b对c的余数相同,则有
同余定理: ( a − b ) % c = 0 同余定理:(a-b)\%c=0 同余定理:(a−b)%c=0
此外常见的类型范围如下:
int 32 范围是 -2,147,483,648 到 2,147,483,647
unsigned int 32 范围是 0 到 4,294,967,295
int 64 (long long) 最大是-9,223,372,036,854,775,808 到 9,223,372,036,854,775,807
2. C++的位运算
- 常见的位运算有6种:与,或,非,异或,左移和右移
- 与运算:
x & 0 = 0
;
- 或运算:
x | 0 = x
;
- 异或运算:
x ^ x = 0
;x ^ 0 = x
;
- 二进制串
S
和二进制串a
异或:S = S ^ a
;- 可以从
S
中消去异或添加的a
;
- 二进制串
S
和二进制串a
与:(S & a) == a
;- 可以判断
S
中是否有或添加的a
; S = S & (~a)
;- 可以从
S
中消去或添加的a
,但仅当或添加均无重叠的1时才有效;
3. 关于溢出的加法运算处理技巧
- 一个是求两个数的平均值:用
medium = low + (high - low) / 2
代替medium = (low + high) / 2
; - 一个是两个数之和的比较:用
nums[i] < target - nums[j]
代替nums[i] + nums[j] < target
; - 也就是说,尽量用减法代替两数之间的加法运算,因为两个正数相加可能会溢出,但两个正数相减不会溢出(即使是极端的
0 - 2^31-1
);
4. 关于string的字符串追加
- (1) 如果是追加
char
类型,则只能使用+
运算符重载; - (2) 如果是追加
string
类型,则既可以用+
运算符重载,也可以用append(string&)
函数; - 推荐均使用运算符重载;
5. 关于常用类型最大值和最小值的写法
int
最小值和最大值:min = int(~((unsigned)(-1) >> 1))
,max = int((unsigned)(-1) >> 1)
;int(-1)
的补码是1111111111...111
(32个1,含1个符号位);(unsigned)(-1)
就是取1111111111...111
为原码, 是unsigned_int
的最大值,为2^32-1
;- 右移一位为
011111111...111
(31个1,符号位为0),是int
的最大值,为2^31-1
; - 按位取反是
10000000...000
,是-0
作-2^31
的补码(为了充分利用空间),也是int
的最小值; - 也就是说,
int
类型在存储时用的是补码来存储;
int
最小值和最大值也可以直接写:INT_MIN
和INT_MAX
;long
最小值和最大值也可以直接写:LONG_MIN
和LONG_MAX
;float
最小值和最大值也可以直接写:FLT_MIN
和FLT_MAX
;double
最小值和最大值也可以直接写:DBL_MIN
和DBL_MAX
;
6. 关于long和long long的区别
-
int
类型肯定是32位的; -
long (int)
类型由编译平台决定,可能是32位或者64位; -
long long (int)
类型肯定是64位的; -
因此,如果需要突破
int
的范围,则推荐使用long long
; -
long
一般情况下不使用,因为无法确定它可表示数的范围; -
一些编译器内部定义的别名如下:
char => int8_t
short int => int16_t
int => int32_t
long int => int32_t / int64_t
long long int => int64_t
float => float32_t
double => float64_t
7. 关于bool类型的%d输出值
- 如上代码,dp矩阵为bool类型;
- 输出时,若是如下printf,则均输出一个非0整数,无论是为true还是为false:
printf("%d", dp[i][j]); // 是一个非0整数
printf("%d", int(dp[i][j])); // 是一个非0整数
- 如要输出0/1,则应该如下printf:
printf("%d", dp[i][j] == true); // 若dp[i][j] = true为1
- 只有以下printf为1,也就是说,1对应true,0对应false:
printf("%d", 1 == true); // 是1
printf("%d", 0 == false); // 是1
- true是1,false是0:
printf("%d", true); // 是1
printf("%d", false); // 是0
- 综上,为了稳妥起见,无论是在c还是c++中都还是直接使用int类型代替bool类型比较好;
- 如果真的要使用bool类型,则输出时需要和true和false类型比较,而不要直接输出;
8. 两种关于C++string类型的值比较方式
- 使用
str1.compare(str2) == 0
,如果返回值小于0,则有字典序上的str1 < str2
; - 使用
str1 == str2
,如果有str1 < str2
,则有字典序上的str1 < str2
;
9. 关于priority_queue的自定义cmp
- 推荐使用仿函数的形式;
- 仿函数的定义如下:
// 仿函数
struct cmp {
bool operator() (const T& a, const T& b) {
return a严格小于b的条件;
}
};
priority_queue
的定义如下:
priority_queue<T, vector<T>, cmp> heap;
- 有三个参数类型的说明;
- 因为
priority_queue
是vector
的配接器,所以要显式给出vector
的类型; - 不是
cmp()
,而是直接传入整个仿函数类类型cmp
; - 如果是标准类型,如
int
,也可以直接用STL中的标准greater
仿函数,用法如下:
priority_queue<int, vector<int>, greater<int>> heap;
- 默认的
priority_queue
是大顶堆,最大值在堆顶;- 如果使用标准仿函数,则:
less
仿函数:大顶堆;greater
仿函数:小顶堆;
- 如果使用自定义仿函数,则:
- 从小到大排:大顶堆;
- 从大到小排:小顶堆;
- 如果使用标准仿函数,则:
10. 关于sort函数的自定义cmp
bool
类型返回值;- 传入两个参数
a
和b
应当用const
和引用&
修饰,以减少空间和时间开销(很重要!); - 为
true
代表a
排在b
前面; - 如果是成员函数,应当使用
static
+private
修饰,否则则不需要; vector
和string
类型都可以用sort
进行排序;- 一个例子如上所示;
- 特别注意的是:为
true
的时候必须是严格小于,=
的情况下不能返回true
值,否则存在相等值时递归的过程可能会陷入死循环或者空递归;
11. 关于list的迭代器用法
-
list
的插入insert()
或删除erase()
不影响除插入或删除外的元素的迭代器; -
list
的迭代器实际上是双向环状链表的节点指针; -
list.end()
返回的迭代器指向list
的最后一个节点,但该节点并不存放实际数据;- 如果想取最后一个元素的迭代器,需要用
std::prev(list.end())
,即取list.end()
的前一个元素(存放数据的最后一个元素)的迭代器; - 如果要设置某个迭代器无效,也可以将它的值设置为
list.end()
,之后判断的时候将该迭代器和list.end()
比较即可;
- 如果想取最后一个元素的迭代器,需要用
-
splice()
可以用于调整链表的结构,可以尝试多使用; -
一些使用如下:
std::list<int> mylist;
// 获得最尾端元素的迭代器
std::list<int>::iterator iter = std::prev(mylist.end());
// 设置迭代器无效
iter = mylist.end();
if(iter == mylist.end()) {
// 迭代器无效的处理
}
// splice函数,其中的other也可以是当前list对象
// 将other中的所有元素移动到当前list的pos指向的节点之前,后other置空
void splice(const_iterator pos, list& other);
// 将other中的it指向的节点移动到当前list的pos指向的节点之前
void splice(const_iterator pos, list& other, const_iterator it);
// 将other中的[first, last)指向的节点移动到当前list的pos指向的节点之前
void splice( const_iterator pos, list& other,
const_iterator first, const_iterator last);
// 将cache_list的iter指向的元素移动到cache_list.end()之前
// 即将iter指向的元素移动到cache_list末尾
cache_list.splice(cache_list.end(), cache_list, iter);
- 一些常见用法的示例如下:
class Node {
int value;
};
list<Node> test_list;
list<Node>::iterator iter = test_list.begin(); // 指向开头元素
// 注意:
//begin()和end()取的是迭代器
//front()和back()取的是节点对象
Node node = *iter; // 取节点值
int value = iter->value; // 取节点内元素
iter = test_list.end(); // 迭代器置空
if(iter == test_list.end()) ... // 迭代器判空
test_list.emplace_back(node);
iter = std::prev(test_list.end()); // 取尾元素的迭代器
// 等价于
iter = test_list.end();
--iter;
12. C++默认的强制数据类型转换
- 在等式右方的表达式计算过程中,强制类型转换遵循以下规则:
- (1)
char
、short
和unsigned short
将会先被强制转换为int
类型再参与计算,无论与它们一起计算的类型是什么; - (2) 剩下的类型将统一被强制转换到二元运算符两边排名更高的类型再参与二元运算,排名的次序如下:
- (1)
- 将等式右方表达式的值赋予左方时,强制类型转换遵循以下规则:
- (3) 将等式右方的值强制转换为等式左方的变量类型;
13. string和普通类型的转换
- (1) string转普通类型:
- C++风格:
- 引入
<string>
库; - 传入的参数是
std::string
类型;
- 引入
- C风格:
- 引入
<stdlib.h>
库; - 传入的参数的
const char*
类型;
- 引入
- C++风格:
/*########## 以下是C++风格的转换 ##########*/
#include <string>
// 1. 以base进制返回从str转换成int类型的值
// idx用于记录str中数字后面的第一个字符的位置
int stoi(const string& str, size_t* idx = 0, int base = 10);
// 例子:
std::string str = "40c3";
std::string::size_type sz; // alias of size_t
int a = std::stoi(str);
int b = std::stoi(str, &sz);
int c = std::stoi(str, nullptr,16);
// 2. 以base进制返回从str转换成long类型的值
long stol(const string& str, size_t* idx = 0, int base = 10);
// 3. 以base进制返回从str转换成unsigned long类型的值
unsigned long stoul(const string& str, size_t* idx = 0, int base = 10);
// 4. 以base进制返回从str转换成long long类型的值
long long stoll(const string& str, size_t* idx = 0, int base = 10);
// 5. 以base进制返回从str转换成float类型的值
float stof(const string& str, size_t* idx = 0);
// 6. 以base进制返回从str转换成double类型的值
double stod(const string& str, size_t* idx = 0);
/*########## 以下是C风格的转换 ##########*/
#include <stdlib.h>
// 1. 将str字符串转换成int类型
int atoi(const char* str);
// 2. 以base进制将str字符串转换成long int类型
// endptr用于记录str中数字后面的第一个字符的位置
long int strtol(const char *str, char **endptr, int base);
// 例子:
std::string str = "40c3";
int a = atoi(str.c_str());
char *ptr;
long b = strtol(str.c_str(), &ptr, 10);
- (2) 普通类型转string:
- C++风格:
- 引入
<string>
库; to_string()
已经重载了常见类型的参数,直接传入普通类型即可;
- 引入
- C++风格:
// 例子:
std::string str = std::to_string(28.5);
14. 使用自定义类型作为map的key
- (1) 作为
std::map
的key值,- 必须重载
<
运算符或者传入自定义比较仿函数;<
运算符或者自定义比较仿函数用于底层红黑树排序;
- 并不需要重载
=
运算符,- 因为可以使用
!a<b && !b<a
代替;
- 因为可以使用
- 必须重载
// 1. 重载<运算符
struct MyType {
int a;
int b;
bool operator<(const MyType& other) const {
if (a == other.a) {
return b < other.b;
}
return a < other.a;
}
};
std::map<MyType, ValueType> my_map;
// 2. 使用自定义比较仿函数
struct MyType {
int a;
int b;
};
struct MyTypeComparator {
bool operator()(const MyType& lhs, const MyType& rhs) const {
if (lhs.a == rhs.a) {
return lhs.b < rhs.b;
}
return lhs.a < rhs.a;
}
};
std::map<MyType, ValueType, MyTypeComparator> my_map;
- (2) 作为
std::unordered_map
的key值,- 必须重载
==
运算符和增加计算整个自定义类型哈希值的哈希函数;==
运算符用于比较两个key之间是否相等;- 哈希函数用于映射key到哈希索引上;
- 不需要重载
<
运算符,- 因为底层不是有序结构;
- 必须重载
struct MyType {
int a;
int b;
bool operator==(const MyType& other) const {
return a == other.a && b == other.b;
}
};
// 这里使用了std提供的哈希类模板偏特化,可以避免在下面的构造函数中传入哈希函数
namespace std {
template <>
struct hash<MyType> {
size_t operator()(const MyType& key) const {
return hash<int>()(key.a) ^ hash<int>()(key.b);
}
};
}
std::unordered_map<MyType, ValueType> my_map;
二、《C++ STL源码剖析》补充
- 简单记录一些《C++ STL源码剖析》中涉及到的C++语法和注意事项;
1. 静态常量成员在类内直接初始化
- 如果含有
static const integral
类型的成员变量,可以在类内声明时直接初始化;
- 注意
integral
不只是int
类型,而是包含所有的整型,如下所示:short
;char
,特别注意char类型也是整型;int
;long
;long long
;
一些关于static
和const
约束的类内成员变量初始化要求:
- 如果只是
static
成员变量,则只能类内声明,然后在类外初始化,如果其他地方需要访问则一定要初始化; - 如果只是
const
成员变量,则可以在类内声明时初始化,也可以在构造函数中初始化,但一定要初始化; - 如果是
static const float/double
则只能在类外初始化,且一定要初始化; static const integral
可以在类内定义时初始化,也可以在类外初始化,但一定要初始化;
需要进行初始化的原因是:
- 类内声明只是做声明,并未分配内存;
- 类外定义才会为该成员变量分配内存空间,并作初始化;
- 如果在其他地方访问了未初始化的静态成员变量,则编译器是不能通过的,一定会报错;
- 但如果其他地方没有访问未初始化的静态成员变量,则编译器可以通过;
- 即使编译器能够通过,也仍然需要小心,因为该静态成员变量是未分配内存空间的,它的构造函数和析构函数是不会调用的;
可以参考博客:C++ 的类中static和const关键字声明变量的初始化方式总结
2. union类型
- union是C/C++中的一种类型,类型中的所有变量共用同一段内存,存储的起点均一致,大小为占用空间最大的变量的大小;
- 因此一个union变量同一时间中只能存其中一个变量的值,其余变量的值均被覆盖(或至少覆盖一部分);
- 目的是减少存储所需的空间,提高空间的利用率;
- 一个例子如下:
- 上图中的
obj
和client_data
共用相同起始地址的一段空间,如下图所示:
- 但要注意尽量不要在union中定义C++风格的
class
类型对象,而是用C风格的struct
或者基本类型,避免出错; - 可以参考博文:C++中的union介绍;
- C风格的类型也被称为POD (Plain Old Data)类型,包括基本数据类型、指针、union、数组、POD的struct或者class;
- POD的struct或者class需同时满足以下条件:
- 不显式定义构造函数、拷贝构造函数、重载赋值函数和析构函数;
- 类内无非静态非POD成员变量;
- 无基类;
3. typedef用法
- 用于为现有的类型创建一个新名字;
- 只为类型增加别名,便于使用;
- typedef并不创建新的类型,仅让新别名在编译时被解释,让编译器进行超越预处理器能力的文本替换而已;
- 使用方法如下:
typedef existing_type new_type_name;
- 和基本类型的使用,例如:
/*改写前*/
unsigned int a;
/*改写后*/
typedef unsigned int WORD;
WORD a;
- 和指针的使用,例如:
/*改写前*/
char * str;
/*改写后*/
typedef char * pstr;
pstr str = "abc";
- 和数组的使用,例如:
/*改写前*/
char line[81];
char text[81];
/*改写后*/
typedef char Line[81];
Line text, secondline;
- 和函数指针的使用,例如:
void printHello(int i);
/*改写前*/
void (*pFunc)(int);
pFunc = &printHello;
(*pFunc)(110);
/*改写后*/
typedef void (*PrintHelloHandle)(int);
PrintHelloHandle pFunc;
pFunc = &printHello;
(*pFunc)(110);
- 参考博文:typedef 用法总结;
4. typename用法
- 作用是用来声明后面跟的
name
是类型名而不是变量名; - 在旧用法中是可以用
class
代替typename
的; - 在泛型中的用法如下:
template<typename T>
class test_typename{
//...
};
template<class T>
class test_class{
//...
};
- 但如果要在
typedef
中使用泛型元素,则必须先使用typename
声明该泛型元素,以表明元素为类型,因为typedef
只能为类型赋别名; - 参考博文:typename的两种用法;
5. explicit用法
- 只能修饰只有一个参数的类构造函数;
- 作用是取消在类对象构造时自动的隐式转换,避免引起语义的漏洞,导致难以察觉的构造错误;
- 注意:
- 使用
explicit
修饰的构造函数的参数即使有默认参数,也不会再隐式转换和调用,相当于是默认参数无法生效,仍然需要传入该参数才能正确调用构造函数; - 复制构造函数和移动构造函数通常不能使用
explicit
修饰,否则函数的传参值复制和返回值复制就不能隐式进行了;
- 使用
- 一个例子如下:
class CxString
{
public:
char *_pstr;
int _size;
// 使用关键字explicit的类声明, 显式转换
// 限定了只能用以下方式调用:
// CxString string1(24);
// 以下调用无法通过编译:
// CxString string2 = 10; // 相当于是将CxString string1(10)封装后再赋值给string2
// CxString string3 = 'c'; // 相当于是将CxString string1(int('c'))封装后再赋值给string3
explicit CxString(int size)
{
_size = size;
// ...
}
};
- 可以参考博文:C++中的explicit详解;
6. 指针常量和常量指针
- 指针常量
- 指针类型的常量;
int * const ptr
;- 指针的指向不可修改,在定义时必须同时初始化;
- 但指针指向的空间内容可以修改;
- 常量指针
- 指向常量的指针;
const int *ptr
或者int const *ptr
;- 指针指向的空间内容不可修改;
- 但指针的指向可以修改;
7. copy_backward()函数用法
- 和
copy()
类似,用于复制一段序列,且只用于序列容器; - 复制时从这段序列的尾部开始复制,避免覆盖后面的值;
- 可以用于将某段序列整体后移,相比之下,
copy()
用于将序列整体前移;
8. void*指针
void*
占据和其他指针相同空间大小,只是它的指向暂时还不明;- 可以强制转换成任何类型的指针,也可以赋值为任何类型的指针,如:
void *pv;
int i = 5;
int *pi = &i;
// 1. void*可以赋值为任意类型的指针
pv = pi;
// 但反过来不行
// pi = pv; // 会出错
// 加上强制类型转换可以
pi = (int *)pv;
// 2. void*可以经过强制类型转换来使用
int j = *(int *)pv;
// 但一定要经过强制类型转换才能使用
// int j = *pv; // 会出错
- 取值不可以不经过强制转换;
- 其他类型的指针也不可以用未经强制转换的
void*
来赋值; - 可以赋初始值为
0
或者nullptr
; - 如果作为函数的参数类型或者返回类型,则表示可以接收任意类型的指针或者可以返回任意类型的指针;
- 相当于是任意普通类型指针的基类,用于实现多态或者充当泛型;
- 可以参考博文:C++中的void*理解;
9. 自增和自减函数重载
- 自增函数重载:
self& operator++() {}
相当于++i
,前置自增,先自增再返回;- 返回的是自身的引用,可以作为左值和右值;
++i = a; // 虽然这样写很奇怪
self operator++(int) {}
相当于i++
,后置自增,先返回再自增;- 返回的是值,只能作为右值;
a = i++;
- 其中的
int
是没有意义的,仅用于区分前置还是后置; - 自减函数重载同理;
三、其他补充
1. strcpy、memcpy和memmove的区别
-
参考:
-
(1)
strcpy()
:- 从src拷贝字符串到dest指向的位置,如果遇到
\0
则提前结束拷贝; - 只能拷贝字符串类型;
- 从src拷贝字符串到dest指向的位置,如果遇到
char *strcpy(char *dest, const char *src);
// 最多拷贝n字节的字符串
char *strncpy(char *dest, const char *src, size_t n);
- (2)
memcpy()
:- 从s2指向的位置拷贝n字节到s1指向的位置;
- 可以拷贝任意内存;
- 假设s1和s2指向的内存区域没有数据重叠;
- 拷贝效率高;
void *memcpy(void *restrict s1, const void *restrict s2, size_t n);
-
补充:restrict用法(C99标准)
- 作用:
- 用于修饰指针;
- 表明在该指针的生命周期内,指向的内存空间不会和别的指针重叠,也就是说,只能通过当前指针访问该内存空间;
- C++不支持;
- 作用:
-
(3)
memmove()
:- 从s2指向的位置拷贝n字节到s1指向的位置;
- 可以拷贝任意内存;
- 允许s1和s2指向的内存区域有数据重叠;
- 在拷贝前作内存检查;
- 如果存在数据重叠,则用一个临时空间作为拷贝的中转;
- 拷贝效率低于
memcpy()
;
void *memmove(void *s1, const void *s2, size_t n);
2. emplace和push的区别
-
(1)
push()
和emplace()
的作用均相同,均是往容器中压入一个元素;push()
是C++11之前的函数,包括:push()
;push_back()
;push_front()
;
emplace()
是C++11之后的函数,相对应push()
,也分别包括:emplace()
;emplace_back()
;emplace_front()
;
-
(2) 如果它们是往容器中压入一个对象,则无论对象是左值还是右值,它们的效果均相同;
- 压入左值对象(也就是该对象已有),则调用拷贝构造函数;
- 压入右值对象(也就是该对象临时),则调用移动构造函数;
class A{
public:
A() { printf("construct A.\n");}
A(int p) {printf("one param construct A.\n");}
A(int p, int q) {printf("double params construct A.\n");}
A(const A& a) {
printf("copy construct A.\n");
}
A(A&& a) {
printf("move construct A.\n");
}
~A() {
printf("destroy A.\n");
}
};
A a;
queue<A> q;
// 左值对象
q.push(a);
printf("seperator ######\n");
q.emplace(a);
printf("seperator ######\n");
/* 输出如下:
copy construct A.
seperator ######
copy construct A.
seperator ######
*/
// 右值对象
q.push(A());
printf("seperator ######\n");
q.emplace(A());
printf("seperator ######\n");
/* 输出如下:
construct A.
move construct A.
destroy A.
seperator ######
construct A.
move construct A.
destroy A.
seperator ######
*/
- (3) 如果是直接传入参数供容器构造,则:
push()
仅接受一个参数,执行:- 调用构造函数构造临时对象;
- 调用移动构造函数压入到容器中;
- 销毁临时对象;
emplace()
可以接受一个或者多个参数,均只执行:- 在容器中申请空间,然后直接调用构造函数构造对象;
- 这种情况下,
emplace()
的效率高于push()
;
- 因此,推荐都使用
emplace()
来代替push()
,这样在某些情况下能够获得更高的效率;- 主要是因为C++11前并没有引入变长参数模板,因此多于一个参数的构造只能是先在外部构造再压入容器;
- 而C++11之后引入了变长参数模板,所以可以直接在压栈的函数中实现构造;
// 传入一个参数
q.push(5);
printf("seperator ######\n");
q.emplace(5);
printf("seperator ######\n");
/* 输出如下:
one param construct A.
move construct A.
destroy A.
seperator ######
one param construct A.
seperator ######
*/
// 传入多个参数
//q.push(4, 6); // 编译不能通过
printf("seperator ######\n");
q.emplace(4, 6);
printf("seperator ######\n");
/* 输出如下:
seperator ######
double params construct A.
seperator ######
*/
- 另外,
emplace()
也可以用于代替insert()
:- 原理和
push()
和emplace()
相同,主要是利用了emplace()
的可变参数避免先构造一个临时对象; - 但在
map
和unordered_map
容器中不建议用emplace()
代替insert()
,因为会引起歧义;
- 原理和
struct Foo {
Foo(int n, double x);
};
std::vector<Foo> v;
v.emplace(someIterator, 42, 3.1416); // 没有临时变量产生
v.insert(someIterator, Foo(42, 3.1416)); // 需要产生一个临时变量
v.insert(someIterator, {42, 3.1416}); // 需要产生一个临时变量
3. 尾递归
-
是一种程序编程技巧:
- 仅在当前递归函数的return关键字后调用递归函数;
- 且调用递归函数后不再执行当前函数的任何指令;
-
作用:
- 可以避免用户栈的递归开销;
- 因为函数调用是当前函数的最后一条指令,所以无需在用户栈上开辟新空间保存当前函数的状态,直接销毁当前函数的用户栈空间即可;
- 因此,在尾递归的过程中,最多仅消耗一个栈帧即可完成递归过程;
- 可以避免用户栈的递归开销;
-
参考:
4. 野指针和悬空指针
-
(1) 野指针:
- Wild pointer;
- 未被初始化过的指针;
- 置空之后就不算未被初始化过;
-
(2) 悬空指针:
- Dangling pointer;
- 指向的内存被释放的指针;
- 置空之后就不是悬空指针;
5. const和constexpr
- (1)
const
:- 表示只读(read-only)语义;
- 修饰变量:
- 变量可以在运行期再初始化;
- 只保证变量在运行时不被修改,但该变量可以是运行期常量;
- 修饰函数:
- 只能修饰类的成员函数;
- 表示该函数不修改类的成员变量;
const int kSize = 1; // 编译期常量
const int kRandomNumber = get_a_random_number(); // 运行期常量
- (2)
constexpr
:- 表示常量(const)语义:
- 修饰变量:
- 变量必须是在编译期就初始化;
- 是真正意义上的常量,只能是编译期常量;
- 修饰函数:
- 可以修饰普通函数;
- 如果传入的参数均为编译期常量:
- 则函数的返回值也在编译期确定;
- 此时函数的返回值也是一个编译期常量;
- 否则,则和普通函数一样,函数返回值需要在运行时才能确定,是一个变量;
constexpr int foo(int i)
{
return i + 5;
}
int main()
{
int i = 10;
std::array<int, foo(5)> arr; // foo函数返回值是编译期常量
foo(i); // Call is Ok
// But...
std::array<int, foo(i)> arr1; // foo退化成普通函数,不能作为常量使用
}