坑点/技巧总结
不要连续使用比较符
if (i < j < k) // 若k大于1则为真
建议使用++i
原因:i++需要将原始值保留下来,会造成浪费。如果是迭代器类型,这种操作消耗就很大了
运算对象可按任意顺序求值
i = fa(x) + fb(y); //fa和fb执行顺序不确定
*beg = toupper(*beg++); //错误:该赋值语句未定义
运算表达式中各项执行顺序不固定,上面第二个,编辑器有两种处理:
1、beg = toupper(beg); //先处理左边
2、(beg+1) = toupper(beg); //先处理右边
条件运算符优先级非常低
cout << grade < 60 ? 1 : 0; //错误: 试图比较cout和60
模板类定义和实现不能分开
C++中每一个对象所占用的空间大小,是在编译的时候就确定的,在模板类没有真正的被使用之前,编译器是无法知道,模板类中使用模板类型的对象的所占用的空间的大小的。只有模板被真正使用的时候,编译器才知道,模板套用的是什么类型,应该分配多少空间。这也就是模板类为什么只是称之为模板,而不是泛型的缘故。
悬垂else
无花括号的if-else,且if多于else时,else匹配最近的未被匹配的if
int a = 6;
int b = 6;
if (a > 5)
if (b > 7)
cout << "log1" << endl;
else
cout << "log2" << endl;
// 最终输出log2
不要返回局部对象的引用或指针
因为函数结束后,局部变量就被释放掉了。
内存耗尽的new
内存耗尽时,new会抛出std::bad_alloc错误
但是我们可以改变new的使用方式来阻止
int *p1 = new (nothrow) int; //如果失败,返回一个空指针
赋值运算符
通常实现是:先深拷贝右侧的值到临时变量,然后销毁左侧的值,再调用左侧的拷贝构造。目的是为了防止两侧指向同一个地址,如果先销毁左侧的值,可能右侧会访问到野指针。
右值引用
为了防止在使用临时变量的过程中产生不必要的数据拷贝,引入了“对象移动”的概念。
简单理解就是移交一些临时变量的所有权,在程序中一些变量的生存期非常短,比如 部分表达式产生的值,后增运算符i++,函数返回值等等,而我们可以在他们即将销毁之前把他们的所有权“移动”(转移)给其他变量,它们在内存中的位置是不变的,也就是没有创建新的对对象,只不过归属于其他变量了,生存期也就随着所属的变量而变化了。
#include <iostream>
#include <string>
std::string foo()
{
return std::string("abc");
}
int main()
{
std::string s3 = foo(); //这样的写法会调用拷贝构造函数
const std::string &s = foo(); //可以使用这个值,但是不能修改他
std::string &&s2 = foo(); //右值引用
}
完美转发
- std::forward()
- std::forward<>()
当用于一个指向模板参数类型的右值引用函数参数(T&&)时,forward会保持实参类型的所有细节
// 完美转发的翻转函数
template <typename F, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 &&t2)
{
f(forward<T2>(t2), forward<T1>(t1));
}
其他tips
- 大的类型变量避免拷贝,因为拷贝效率底,传参的时候,尽可能用引用
- IO对象不能拷贝和赋值(包括作为返回参数)
- forward_list增加和删除接口和其他容器不同,根本原因是无法访问前驱
- list和forward_list优先使用成员函数,而不是通用算法
- 如果我们用不到某个形参,那我们不需要为其命名
- 基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此
- 如果一个函数参数是指向模板类型参数的右值引用(如T&&),它对应的实参的const属性和左右值属性将得到保持
- 调用析构函数或销毁对象,但是不会释放内存