c++新手写代码的几个易错点
学习自
https://www.youtube.com/watch?v=i_wDa2AS_8w
1. 限制 using namespace std
的作用范围
如果你自己定义了一个和std
空间内同名的函数,当你把std
放到global namespace
(也就是直接using namespace std
)中,就会出现函数冲突;
using namespace std
的使用如果仅限于一个局部作用域,使用起来还可以接受;- 不要在
global
范围内使用; - 不要在头文件中使用,这样其他人
include
你的头文件,也被强制使用了这个习惯。
2. 避免在循环中使用std::endl
std::endl
等价于 \n << std::fulsh()
缓冲区,刷新缓冲区会有额外的时间消耗。
可以这样使用std::cout << "hello world" << "\n";
通过\n
来进行换行,在最后的输出中再使用std::endl
把所有的内容一次性刷新了。
3. range-based for loop
替换 index-base for loop
- 类型安全:不需要担心索引可能会超出数组或容器的边界,就比如说
size_t idx = vec.size(); idx >=0; --idx
,idx--
的操作会一直大于0; - 通用性:
range-based for loop
可以用于任何支持begin()
和end()
成员函数的容器,包括数组、链表、集合等。
多说一句operator[]
在不同容器上的不同:
就拿std::vector
、std::map
和 std::unordered_map
举例来说,它们是C++标准库中的不同类型的容器,它们的设计目的和内部数据结构各不相同,这导致了它们在使用下标操作符operator[]
访问元素时性能上的差异。
std::vector
是一个动态数组,它在内存中分配一块连续的空间来存储元素。访问任何一个元素(通过下标)只需要一次内存访问,因为它可以通过基址加偏移量的方式直接计算出元素的内存地址。时间复杂度是O(1)
,是最快的访问方式。std::map
是一个基于红黑树的有序关联容器,它保持其元素按照键排序。当你使用下标操作符来访问元素时,std::map
需要从根节点开始进行一系列比较操作来找到对应的键值。时间复杂度是O(log n)
。std::unordered_map
是一个基于哈希表的无序关联容器,它通过哈希函数来定位元素的位置。使用下标操作符访问元素时,首先需要计算键的哈希值,然后在哈希表中找到对应的桶,最后在桶中遍历元素以匹配确切的键。在理想情况下,如果哈希函数分布良好且没有太多哈希冲突,这个操作的时间复杂度接近O(1)
。但在最坏的情况下,如果所有元素都映射到同一个桶中,时间复杂度会退化到O(n)
。
4. 使用标准库已有的algorithm
,而不是自己去实现一个
毕竟是自己实现的,性能没准比不上标准库已经有的算法。
举个例子,查找数组中大于某个值的元素:
int main() {
std::vector<int> data{-1, 2, 1, 5};
std::size_t first_pos_idx;
for (std::size_t i = 0; i < data.size(); ++i) {
if (data[i] > 0) {
first_pos_idx = i;
break;
}
}
std::cout << data[first_pos_idx] << "\n";
auto first_pos_it =
std::find_if(data.begin(), data.end(), [](int x) { return x > 0; });
std::cout << *first_pos_it << "\n";
}
5. 使用std::array
代替c
风格的数组
使用c
风格的数组,如果要获取数组大小,需要单独维护一个变量记录数组大小。
而sta::array
不需要;同时std::array
也封装了很多函数操作,方便我们直接使用。
6. reinterpret_cast
从reinterpret_cast
获取的对象,你唯一能做的事儿就是:把它重新解释为原类型,解释成其他类型的行为都是未定义的。
long long x = 20;
auto y = reinterpret_cast<char*>(x);
auto z = reinterpret_cast<long long>(y);
7. 尽量不要使用const_cast
来丢掉const
修饰
代码举例:
特别注意,C++的std::unordered_map
使用operator[]
获取元素时,不能是const
修饰的,因为operator[]
在未查找到key
时会插入元素,这样就会修改std::unordered_map
的内容,与const
会冲突。
改为使用at()
即可。
const std::string &
more_frequent(const std::unordered_map<std::string, int> &word_counts,
const std::string &word1, const std::string &word2) {
// 1. 下面operator[]不能用于const类型的变量
// return word_counts[word1] > word_counts[word2] ? word1 : word2;
// 2. 使用const_cast可以下掉const修饰
// auto &counts =
// const_cast<std::unordered_map<std::string, int> &>(word_counts);
// return counts[word1] > counts[word2] ? word1 : word2;
// 3. 使用at(),map重载了类const版的方法
return word_counts.at(word1) < word_counts.at(word2) ? word1 : word2;
}
为啥不重载const
版的operator[]
?因为不知道map[key]
会插入元素还是只是读取元素。
8. map
的operator[]
访问一个不存在的元素会执行插入操作
引自:https://en.cppreference.com/w/cpp/container/map/operator_at
Returns a reference to the value that is mapped to a key equivalent to key, performing an insertion if such key does not already exist.
如果插入的key
不在map
中,就会执行插入元素的操作。
9. 尽量添加const
修饰
- 避免代码中错误地修改了变量内容;
- 从语义上来讲,其他人看到
const
,也知道这个变量是只读的。
const
和constexpr
:
const applies for variables, and prevents them from being modified in your code. 可以避免在你的代码中修改被const
修饰的变量。
constexpr tells the compiler that this expression results in a compile time constant value. 告诉编译器可以在编译期计算出来值。
10. 不知道字符串的生命周期
下面这个字符串在整个程序的生命周期内都是存在的,即使看起来像是返回了一个局部变量。
字符串字面量存放在static
区,声明周期较长。
const char* get_string() {
return "hello world";
}
11. 使用结构化绑定
- 结构化绑定
map
int main() {
std::unordered_map<std::string, int> students{
{"hello", 16},
{"world", 8}
};
for (const auto &student : students) {
std::cout << student.first << "," << student.second << "\n";
}
for (const auto &[name, age] : students) {
std::cout << name << "," << age << "\n";
}
}
- 结构化绑定对象的
public
成员变量
struct Student {
public:
std::string name;
int age;
};
Student get_student() {
return {"hello", 8};
}
int main() {
const auto &[name, age] = get_student();
std::cout << name << "," << age << "\n";
}
12. 想从一个函数返回多个输出时,可以定义一个结构体,包含了这些参数成员
从一个函数返回多个输出时,一般会把这些输出定义到参数内,并设置为引用。当输出较多时,会导致参数量变多。那其实我们就可以把这些输出统一定义到一个结构体内直接返回该结构体即可。
13. 尽量在编译器就完成一些要在运行期完成的工作
// int sum_of_1_to_n(const int n) {
// return n * (n + 1) / 2;
// }
constexpr int sum_of_1_to_n(const int n) {
return n * (n + 1) / 2;
}
void uses_sum() {
const int limit = 1000;
sum_of_1_to_n(limit); // 参数在编译期就已经知道了,那在编译期就可以完成计算
}
14. 忘记将析构函数标记为virtual
如果不加virtual
修饰,基类指针指向子类对象,析构基类指针时,只会析构基类对象,不会析构子类对象。
15. 类构造函数的列表初始化变量的顺序
顺序是按照声明成员变量的顺序,而非显示列表初始化成员的初始化顺序。
16. 默认初始化和值初始化
int main() {
// 默认初始化
int x;
int *x2 = new int;
// 值初始化(即使效率会低一点,也尽量使用这个初始化)
int y{};
int *y2 = new int{};
int *y3 = new int();
// 函数声明
int z();
}
17. 对于magic number
,将其定义为一个常量
A magic number is a direct usage of a number in the code.
- 全局
number
被定义为一个常量,随时可以根据需求重新定义这个值。 - 可以定义一个可读性高的常量名字,提高代码可读性。
18. 尽量不要在循环过程中删除或者添加vector元素
除非你能明确你在erase元素之后,还能正常使用该vector。
因为你不知道在vector容器重新分配内存后,原有的地址空间是否还可用。避免 iter++
访问到非法内存。
int main() {
std::vector<int> persons{1, 2, 3, 4};
for (auto it = persons.begin(); it != persons.end();
/*it++*/) {
if (*it <= 0)
it = persons.erase(it);
else
++it;
}
}
19. 不要在函数中返回move
的局部变量
std::vector<int> make_vector(const int n) {
std::vector<int> v{1, 2, 3, 4};
// 有时候会阻止编译器进行返回值优化
return std::move(v);
}
在C++中,当你从函数返回一个局部对象时,编译器通常会利用返回值优化(Return Value Optimization, RVO)或者命名返回值优化(Named Return Value Optimization, NRVO)来避免不必要的拷贝或移动操作。当你显式地使用std::move
时,你实际上是在告诉编译器放弃这种优化,因为std::move
将对象转换为右值,从而可能导致不必要的移动操作。
20. move 并没有移动了东西
move == cast to rvalue,move只是将传入的参数转换为右值引用,真正的移动操作其实在你实现的移动构造函数和移动赋值运算函数中。
21. evaluation order is not guaranteed to be left to right
在C++中,当我们说一个表达式的evaluation order(求值顺序)是unspecified(未指定),是指编译器可以自由地决定以何种顺序计算表达式中的各个子表达式,而不需要遵循任何特定的顺序。这意味着编译器的选择可能因不同的实现或者甚至是不同的编译时选项而异。
int i = 0;
func(i, i++);
在这个例子中,func
的两个参数是i
和i++
。由于C++标准没有指定这两个参数的求值顺序,编译器可以选择先计算i
或者先计算i++
。这可能导致 func
接收到的参数值不同,因此程序的行为可能会不一致
22. 避免使用不必要的堆分配,使用栈即可
- 尽量分配大的对象时再使用堆分配;
- 尽量避免手动释放资源,依赖析构函数来释放资源;
struct Person {
int age;
std::string name;
};
int main() {
Person *p = new Person{18, "liyanghou"};
// 如果中间发生了异常会导致程序异常退出,那么下面的delete永远不会被执行到
// 就会导致内存泄漏啦
// 所以为了确保资源被释放掉,可以将代码清理放在析构函数中,遵循RAII原则
delete p;
}
23. unique_ptr
和shared_ptr
24. 不应该直接构造一个unique
和shared
指针,建议使用make_unique
和make_shared
struct Person {
int age;
std::string name;
Person(int age, std::string name) : age(age), name(name) {}
};
int main() {
std::shared_ptr<Person> p = std::shared_ptr<Person>(new Person(18, "name"));
std::shared_ptr<Person> p2 = std::make_shared<Person>(18, "name"); // 推荐使用这种
}
对于第一种构造方法,new Person(18, "name")
首先创建了一个Person
对象,然后将这个新创建的对象传递给std::shared_ptr
的构造函数来创建智能指针。这个过程涉及两次分配:一次是为Person
对象本身,另一次是为std::shared_ptr
内部的控制块(用于记录引用计数等信息);
对于第二种构造方法,使用了std::make_shared
函数模板来创建智能指针。std::make_shared
在一个单独的操作中同时分配Person
对象和控制块,这通常会更高效,因为它减少了内存分配的次数,并且可能减少了内存碎片。此外,使用std::make_shared
还可以减少代码中潜在的异常安全问题,因为它确保即使构造函数抛出异常,内存也会被正确地管理。
25. 没必要自己手写在析构函数中释放资源,unique_ptr
和shared_ptr
已经完成了这些事
26. RAII
任何需要自己手动释放的资源,都先查询下是否有可以自动释放的类
27. 有些地方还是可以使用普通指针的 pointer
28. 不要返回一个共享式指针shared_ptr
不确定对象是否会被共享时,不要返回一个shared_ptr
,因为我们不确定用户是否需要的是shared_ptr
还是unique_ptr
unique_ptr
可以很容易地转换为shared_ptr
shared_ptr
不可以被转换为unique_ptr
you can easily and efficiently convert a std::unique_ptr to std::shared_ptr but you cannot convert std::shared_ptr to std::unique_ptr.
举个例子:
// 1.
std::unique_ptr<std::string> unique = std::make_unique<std::string>("test");
std::shared_ptr<std::string> shared = std::move(unique);
// 2.
std::shared_ptr<std::string> shared = std::make_unique<std::string>("test");
29. shared_ptr
不是线程安全的
- 只有引用计数是线程安全的,因为是原子操作;
- 指针指向的内容不是线程安全的
30. const
指针和指向const
的指针
- 谨记,
const
修饰紧靠其左侧的内容; - 如果
const
在最左侧,那么修饰其右侧的内容;