这些知识点你都知道吗,测试你的C++入门程度
大家好,我是程序喵!
做公众号以来,很多人都私信问我如何入门C++,其实之前也有很多人问过这个问题,觉得还是有必要写一篇完整的文章,给大家参考。
既然要写一篇文章,程序喵本着对读者负责任的态度,在写这篇文章前我咨询了好多人,既有身经百战的大佬,也有初入职场的精英码农,我也查阅了好多入门C++相关的资料(发现内容都差不多),结合自身经历,整理出一篇文章分享给大家。
不同人入门C++的方式应该都有所不同,就我自己而言,大一先学习了C语言,稀里糊涂一个学期过去了,用C语言做个控制台界面的学生管理系统,然后大一下学期学习C++时,恰好上了一门编译原理的课,当时实现了个小编译器,做过校园导航系统,小型数据库系统,有点小成就感,原以为算是“C++入门”了吧。现在想想其实很多东西学生时期还是知其然而不知其所以然。之后在工作中使用C++做项目,才是真正意义的“入门”了,所以对我而言,可能更多的是依靠“实战”去入门的C++。
本篇文章我不会全篇介绍入门C++需要看什么书,看什么视频,因为这种文章太多了,相信读者看的已经眼花缭乱了,正文主要介绍C++的一些基础的知识点,基本上对每个知识点我会贴一段示例代码,读者如果要具体了解某个知识点,可以去文末的书籍或视频中针对性学习,我会在文末列出比较容易入门的几本书和视频。老规矩,废话停止,直接开始:
C++是什么?
通俗的讲,可以简单理解为C语言的延伸语言,这里会主要介绍C++相对于C语言来说,有哪些特性。
C++的一些特点:
允许重载:函数名相同,参数类型不同,参数个数不同,即可重载,使用见下面代码:
// overload.cc
#include <iostream>
using std::cout;
void func(int a) { cout << "func " << a << "\n"; }
void func(int a, int b) { cout << "func " << a << " " << b << "\n"; }
void func(float a) { cout << "func " << a << "\n"; }
int main() {
func(1);
func(1, 2);
func(1.1f);
return 0;
}
namespace:namespace是什么?其实就相当于为变量和函数划个范围
如果不加namespace会有什么问题:
// namespace.cc
#include <iostream>
void func() {
std::cout << "n1 func" << "\n";
}
void func() {
std::cout << "n2 func" << "\n";
}
// redefinition of 'func'
int main() {
func();
return 0;
}
结果大家可能都知道,这样的代码编译会报错:redefinition of 'func'
一般规定一个程序内不允许同一个强符号有多个定义,那怎么解决?
可使用namespace:
// namespace.cc
#include <iostream>
namespace n1 {
void func() {
std::cout << "n1 func"
<< "\n";
}
} // namespace n1
namespace n2 {
void func() {
std::cout << "n2 func"
<< "\n";
}
} // namespace n2
// redefinition of 'func'
int main() {
n1::func();
n2::func();
return 0;
}
这样编译成功,加入namespace后其实对编译器来说就是两个不同的函数,因为生成的是两个不同的符号:
注意:不要过多using namespace xxx,例如using namespace std;可能就容易引起符号冲突问题,
可以使用using std::cout这种方式。
class & struct:
很多面试官可能都会问class和struct的区别?具体答案我这里就不贴出来了,大家自己搜索答案就可,或者留言问我,它俩在C++里其实没什么区别,就是默认权限不同而已。
这里需要掌握几个知识点:
如何定义一个类
构造函数的定义及使用
析构函数的定义及使用
拷贝构造函数的定义及使用
移动构造函数的定义及使用
赋值构造函数的定义及使用
移动赋值函数的定义及使用
下面贴出一段示例代码:
// class.cc
#include <iostream>
#include <memory>
using std::cout;
class TClass {
public:
TClass() {
cout << "构造函数1" << "\n";
}
TClass(int a) : a_(a) {
cout << "构造函数2" << "\n";
data_ = new int[10];
}
TClass(const TClass &a) : a_(a.a_) {
cout << "拷贝构造函数" << "\n";
data_ = new int[10];
memcpy(data_, a.data_, 10 * sizeof(int));
}
TClass(TClass &&a) : a_(a.a_), data_(a.data_) {
a.data_ = nullptr;
cout << "移动构造函数" << "\n";
}
TClass &operator=(TClass &a) {
a_ = a.a_;
if (data_) delete[] data_;
data_ = new int[10];
memcpy(data_, a.data_, 10 * sizeof(int));
cout << "赋值构造函数" << "\n";
return *this;
}
TClass &operator=(TClass &&a) {
a_ = a.a_;
if (data_) delete[] data_;
data_ = a.data_;
a.data_ = nullptr;
cout << "移动赋值函数" << "\n";
return *this;
}
~TClass() {
cout << "析构函数" << "\n";
if (data_) delete[] data_;
}
void func() { cout << "a " << a_ << " \n"; }
private:
int a_;
int *data_;
};
int main() {
TClass a(1); //构造函数2
TClass b(a); //拷贝构造函数
TClass c(std::move(a)); //移动构造函数
c = b; //赋值构造函数
c = std::move(b); //移动赋值函数
TClass d = c; //拷贝构造函数
TClass e = std::move(d); //移动构造函数
a.func();
b.func();
return 0;
}
模板
模板主要分为函数模板和类模板,下面有示例代码:
// template.cc
#include <iostream>
using std::cout;
template <typename T>
T max(T a, T b) { // 函数模板
return a > b ? a : b;
}
template <typename T>
struct Vec { // 类模板
Vec(T a) : a_(a) {}
void func() { cout << "func value " << a_ << "\n"; }
T a_;
};
int main() {
cout << "max(1, 2) " << max(1, 2) << "\n";
cout << "max(1.1f, 2.2f) " << max(1.1f, 2.2f) << "\n";
cout << "max(1.10, 2.20) " << max(1.10, 2.20) << "\n";
Vec<int> vi(1);
vi.func();
Vec<float> vf(1.1f);
vf.func();
return 0;
}
引用:C++里多了个引用的概念,很多人面试应该有被问到过引用和指针有什么区别吧?
关于指针和引用,在这里强烈推荐大家阅读《面试系列之指针和引用的使用场景》这篇文章,通俗易懂的纯干货型文章。
大家可以记住一个关键点就是引用追求从一而终,它的指向永远不会变,而指针是个善变的东西,它的指向随时可以改变,一般开发中会使用const &方式进行参数传递,省去不必要的对象拷贝:
void func(const A& a) {}
多态:这是C++语言所支持的或者说面向对象的一个重要特性,直接看代码:
// polymorphic.cc
#include <iostream>
using std::cout;
struct Base {
Base() {
cout << "base construct" << "\n";
}
virtual ~Base() {
cout << "base destruct" << "\n";
}
void FuncA() {}
virtual void FuncB() { cout << "Base FuncB \n"; }
int a;
int b;
};
struct Derive1 : public Base {
Derive1() { cout << "Derive1 construct \n"; }
~Derive1() { cout << "Derive1 destruct \n"; }
void FuncB() override { cout << "Derive1 FuncB \n"; }
};
struct Derive2 : public Base {
Derive2() { cout << "Derive2 construct \n"; }
~Derive2() { cout << "Derive2 destruct \n"; }
void FuncB() override { cout << "Derive2 FuncB \n"; }
};
int main() {
{
Derive1 d1;
d1.FuncB();
}
cout << "======= \n";
{
Derive1 d1;
d1.FuncB();
Base &b1 = d1;
b1.FuncB();
}
cout << "======= \n";
{
Derive2 d2;
d2.FuncB();
Base &b2 = d2;
b2.FuncB();
}
cout << "======= \n";
{
Base b;
b.FuncB();
}
cout << "======= \n";
{
Base *b = new Derive1();
b->FuncB();
delete b;
}
return 0;
}
这里简单介绍了实现多态的两种方式:通过指针或者通过引用。
注意:
构造函数不能是虚函数
基类析构函数最好是虚函数
尽量不要使用多继承
new和delete
C++内存申请和释放会使用new和delete关键字,而基本不会使用C语言中的malloc和free,可看下面的示例代码:
// new_delete.cc
#include <iostream>
using std::cout;
struct A {
int a_;
};
int main() {
A* a1 = new A;
delete a1;
A* a2 = new A[10];
delete[] a2;
return 0;
}
注意:
new和delete要配对使用
new[]和delete[]要配对使用
maloc和free要配对使用
类型转换:既然使用了C++语言,在类型转换方面就一定要使用C++风格,这比C语言风格的强制类型转换更安全。
static_cast
float f = 1.0f;
int a = static_cast<int>(f);
const_cast
const char* cc = "hello world\n";
char* c = const_cast<char*>(cc);
dynamic_cast
struct Base {};
struct Derive : public Base {};
void func() {
Base* base = new Derive;
Derive* derive = dynamic_cast<Derive*>(base);
}
reinterpret_cast
A *a = new A;
void* d = reinterpret_cast<void*>(a);
C++11常用新特性
C++的重大变革肯定是C++11啦,C++11标准引入了很多有用的新特性,这仿佛打开了新世界的大门,让C++开发者开发效率大幅提高,下面我会列出C++11常用的新特性,并附上简单的实例代码:
auto & decltype:用于类型推导
auto用于推导变量类型,decltype用于推导表达式返回值类型
// auto_decltype.cc
int main() {
auto a = 10; // 10是int型,可以自动推导出a是int
int x = 0;
decltype(x) y; // y是int类型
decltype(x + y) z; // z是int类型
return 0;
}
std::function:用于封装一个函数,功能类似于函数指针,但却比函数指针方便的多。
// function.cc
#include <functional>
#include <iostream>
using std::cout;
void printNum(int i) { cout << "print num " << i << "\n"; }
typedef void (*FuncPtr)(int i);
int main() {
FuncPtr ptr = printNum;
ptr(2);
void (*ptr2)(int) = printNum;
ptr2(3);
std::function<void(int)> func = printNum;
func(1);
return 0;
}
lambda表达式:随时可方便定义的一个匿名函数。
// lambda.cc
#include <iostream>
using std::cout;
int main() {
auto func = [](int a) -> int { return a + 1; };
int b = func(2);
cout << b << "\n";
return 0;
}
注意:lambda表达式的变量捕获方式分为值捕获和引用捕获
int a = 0;
auto f1 = [=](){ return a; }; // 值捕获a
cout << f1() << endl;
auto f2 = [=]() { return a++; }; // 修改按值捕获的外部变量,error
auto f3 = [=]() mutable { return a++; };
std::function和std::bind使得我们平时编程过程中封装函数更加的方便,而lambda表达式将这种方便发挥到了极致,可以在需要的时间就地定义匿名函数,不再需要定义类或者函数等,在自定义STL规则时候也非常方便,让代码更简洁,更灵活,开发效率也更高。
std::thread:C++11中使用std::thread创建线程,使用非常的方便,直接看代码:
void func() { std::cout << "new thread \n"; }
int main() {
std::thread t(func);
if (t.joinable()) {
t.join(); // 或者t.detach();
}
return 0;
}
注意:使用std::thread一定要记得join或者detach。
RAII:既然使用C++,那一定要理解RAII风格(利用对象生命周期管理资源),继续往下看:
如果不使用RAII风格,加锁解锁我们怎么办?
// 不使用RAII
void func() {
std::mutex mutex;
mutex.lock();
if (xxx) {
mutex.unlock();
return;
}
if (xxxx) {
mutex.unlock();
return;
}
...
mutex.unlock();
}
而如果使用RAII呢:
void func() {
std::unique_lock<std::mutex> lock(mutex);
if (xxx) return;
if (xxxx) return;
...
}
是不是方便了很多,这里介绍了std::unique_lock的使用,还有一种锁是std::lock_guard,使用方式相同,至于它们之间有什么区别,我这里卖个关子,大家可以自行查找哈,锻炼一下自己的搜索能力。
智能指针也是典型的RAII风格,如果不使用智能指针管理内存是这样:
void func() {
A* a = new A;
if (xxx) {
delete a;
return;
}
if (xxxx) {
delete a;
return;
}
...
delete a;
}
而如果使用智能指针是这样:
void func() {
std::shared_ptr<A> sp = std::make_shared<A>(); // unique_ptr类似
std::shared_ptr<A> sp = std::shared_ptr<A>(new A); // 尽可能使用make_shared或者make_unique(C++14)
if (xxx) return;
if (xxxx) return;
...
}
又方便了很多吧,unique_ptr和shared_ptr的区别本文也不介绍,文章最后我会列出学习资料,在学习资料里可以找到答案。
原子操作:使用std::atomic可达到原子效果,使用方法:
std::atomic<int> ai;
ai++;
ai.store(100);
int a = ai.load();
enum class:带有作用域的枚举类型,可用于完全替代enum,为什么要使用enum class呢?我们先看一段直接使用enum的代码:
enum AColor {
kRed,
kGreen,
kBlue
};
enum BColor {
kWhite,
kBlack,
kYellow
};
int main() {
if (kRed == kWhite) {
cout << "red == white" << endl;
}
return 0;
}
不带作用域的枚举类型可以自动转换成整形,且不同的枚举可以相互比较,代码中的kRed居然可以和kWhite比较,这都是潜在的难以调试的bug,而这种完全可以通过有作用域的枚举来规避。
所以出现了enum class:
enum class AColor {
kRed,
kGreen,
kBlue
};
enum class BColor {
kWhite,
kBlack,
kYellow
};
int main() {
if (AColor::kRed == BColor::kWhite) { // 编译失败
cout << "red == white" << endl;
}
return 0;
}
使用enum class可以在编译层面就规避掉一些难以调试的bug,使代码健壮性更高。
condition_variable:条件变量
std::condition_variable cv;
std::mutex mutex_;
void func1() {
std::unique_lock<std::mutex> lock(mutex_);
--count;
if (count == 0) cv.notify_all();
}
void func2() {
std::unique_lock<std::mutex> lock(mutex_);
while (count) {
cv.wait(lock);
}
}
注意:不要直接用wait,而要记得使用wait(mutex, cond)或者while(cond) {wait(mutex);}
nullptr:表示空指针可以使用nullptr,不要使用NULL
void func(char*) {
cout << "char*";
}
void func(int) {
cout << "int";
}
int main() {
func(NULL); // 编译失败 error: call of overloaded ‘func(NULL)’ is ambiguous
func(nullptr); // char*
return 0;
}
chrono:时间相关函数可以考虑使用chrono库,例如休眠某个时间段:
void customSleep() {
std::this_thread::sleep_for(std::chrono::milliseconds(5));
std::this_thread::sleep_for(std::chrono::seconds(5));
}
使用chrono可以精确的指定时间单位,可以明确的之道休眠的是几毫秒还是几秒,非常方便。
chrono中包含三种时钟:
steady_clock:单调时钟,只会增加,常用语记录程序耗时
system_clock:系统时钟,会随系统时间改动而变化
high_resolution_clock:当前系统最高精度的时钟,通常就是steady_clock
拿计时举例:
void customSleep() {
std::this_thread::sleep_for(std::chrono::milliseconds(5));
std::this_thread::sleep_for(std::chrono::seconds(5));
}
int main() {
std::chrono::time_point<std::chrono::high_resolution_clock> begin = std::chrono::high_resolution_clock::now();
customSleep();
auto end = std::chrono::high_resolution_clock::now();
auto diff = end - begin;
long long diffCount = std::chrono::duration_cast<std::chrono::milliseconds>(diff).count();
cout << diffCount << "\n";
return 0;
}
STL
使用C++有几个能不用STL标准库的,这里列出了常用的STL,并贴出使用代码。
std::vector:可以理解为动态可自动扩容的数组,使用vector需要掌握几个常用函数的使用
resize
reserve
capacity
clear
swap
at
灵活使用
void func() {
std::vector<int> vec;
std::cout << "vector size " << vec.size() << "\n"; // vector size 0
std::cout << "vector capacity " << vec.capacity() << "\n"; // vector capacity 0
vec.push_back(1);
vec.emplace_back(2);
vec.push_back(3);
std::cout << "====================== \n";
std::cout << "vector size " << vec.size() << "\n"; // vector size 3
std::cout << "vector capacity " << vec.capacity() << "\n"; // vector capacity 4
vec.reserve(200);
std::cout << "====================== \n";
std::cout << "vector size " << vec.size() << "\n"; // vector size 3
std::cout << "vector capacity " << vec.capacity() << "\n"; // vector capacity 200
vec.resize(20);
std::cout << "====================== \n";
std::cout << "vector size " << vec.size() << "\n"; // vector size 20
std::cout << "vector capacity " << vec.capacity() << "\n"; // vector capacity 200
vec.clear();
std::cout << "====================== \n";
std::cout << "vector size " << vec.size() << "\n"; // vector size 0
std::cout << "vector capacity " << vec.capacity() << "\n"; // vector capacity 200
std::vector<int>().swap(vec);
std::cout << "====================== \n";
std::cout << "vector size " << vec.size() << "\n"; // vector size 0
std::cout << "vector capacity " << vec.capacity() << "\n"; // vector capacity 0
}
注意:for循环中erase某一个节点时要处理好迭代器的指向问题
void erase(std::vector<int> &vec, int a) {
for (auto iter = vec.begin(); iter != vec.end();) { // 正确
if (*iter == a) {
iter = vec.erase(iter);
} else {
++iter;
}
}
for (auto iter = vec.begin(); iter != vec.end(); ++iter) { // error
if (*iter == a) {
vec.erase(iter);
}
}
}
继续注意:remove函数不会真正的删除某个元素,它只会把某个要删除的元素移动到容器尾部,真正要删除还需使用erase,需要remove要和erase搭配使用。
bool isOdd(int i) { return i & 1; }
void print(const std::vector<int>& vec) {
for (const auto& i : vec) {
std::cout << i << ' ';
}
std::cout << std::endl;
}
int main() {
std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
print(v);
std::remove(v.begin(), v.end(), 5); // error
print(v);
v.erase(std::remove(v.begin(), v.end(), 5), v.end());
print(v);
v.erase(std::remove_if(v.begin(), v.end(), isOdd), v.end());
print(v);
}
std::array:开发时可以考虑用std::array代替普通数组,因为它有获取长度,遍历,边界检查等功能
int main() {
std::array<int, 10> array;
// int array[10];
array.at(10) = 20; // terminating with uncaught exception of type std::out_of_range: array::at
std::cout << "hello " << array.at(10) << "\n";
return 0;
}
std::map & std::unordered_map:都是key-value级别的字典,使用方式相同,一个使用树实现,一个使用哈希表实现,可根据需求选择具体使用哪种字典。
int main() {
std::map<int, std::string> map;
map[1] = std::string("hello");
std::cout << map[1] << "\n";
return 0;
}
std::list:链表
int main() {
std::list<int> list{1, 2, 3, 2};
list.sort();
// std::sort(list.begin(), list.end());
for (auto i : list) {
std::cout << i << " ";
}
std::cout << "\n";
return 0;
}
注意:list的排序需要使用list.sort(),不能使用std::sort
std::tuple:我个人经常使用,tuple像结构体一样,也可以理解为pair的延伸
struct T {
int a_;
int b_;
int c_;
T(int a, int b, int c) : a_(a), b_(b), c_(c) {}
};
int main() {
T a(1, 2, 3);
std::cout << "a " << a.a_ << " b " << a.b_ << " c " << a.c_ << "\n";
std::tuple<int, int, int> tuple = std::make_tuple(2, 3, 4);
std::cout << "a " << std::get<0>(tuple) << " b " << std::get<1>(tuple) << " c " << std::get<2>(tuple) << "\n";
return 0;
}
编码规范
每一门语言基本都有几种常见的编码规范,想必大多数C++开发者都会基于google的C++编码规范去开发,下面是格式化代码常用的一些配置:
// .clang-format
BasedOnStyle: Google
IndentWidth: 4
ColumnLimit: 120
SortIncludes: true
MaxEmptyLinesToKeep: 2
下面再列出一些常见的命名规则:
文件命名:文件名字要全部小写,中间用_相连,后缀名为.cc .cpp和.h
hello_world.cc
hello_world.cpp
hello_world.h
类型命名:类型名称的每个单词首字母均大写:
struct MyExcitingClass;
常量和枚举命名:声明为 constexpr 或 const 的变量, 或在程序运行期间其值始终保持不变的, 命名时以 “k” 开头, 大小写混合
const int kDaysInAWeek = 7;
函数命名:大驼峰方式
MyExcitingFunction()
命名空间命名:全部小写
namespace abcdefg{}
成员变量命名:小写字母加下划线分隔,最后加个下划线
普通变量命名:小写字母加下划线分隔
struct A {
int a_;
}
void f(int a) {
int b;
}
代码:
struct A {
int a_;
}
void f(int a) {
int b;
}
规范要点总结
下面是我查阅很多资料又结合自身工作经验总结的一些要点:
每个头文件都要使用#ifndef #define或者#pragma once修饰,避免被重复引用
每个命名都要尽可能表达清晰其具体含义,除非特别常用,否则不要使用缩写,宁可名字特别长
鼓励在 .cc 文件内使用匿名命名空间或 static 声明. 使用具名的命名空间时, 其名称可基于项目名或相对路径. 尽量不要使用 using 指示
一行尽量不要超过120个字符,一个函数尽量不要超过40行(性能优化除外),同时一个文件尽量控制在500行内.
所有的引用形参如不做改动一律加const,在任何可能的情况下都要使用 const或constexpr
new内存的地方尽量使用智能指针,c++11 就尽量用std::unique_ptr
合理使用移动语义或者COW,减少内存拷贝
使用 C++ 的类型转换, 如 static_cast<>(). 不要使用 int y = (int)x 或 int y = int(x) 等转换方式
明确使用前置++还是后置++的具体含义,如不考虑返回值,尽量使用前置++ (++i)
不要使用uint类型,如果需要使用大整型可以考虑int64,否则类型的隐式类型转换会带来很多麻烦
如无特殊必要不要使用宏,可以考虑使用const或constexpr替代宏,宏的全局作用域很麻烦,如果非要用在马上要使用时才进行 #define, 使用后要立即 #undef
尽可能用 sizeof(varname) 代替 sizeof(type).使用 sizeof(varname) 是因为当代码中变量类型改变时会自动更新. 或许会用 sizeof(type) 处理不涉及任何变量的代码,比如处理来自外部或内部的数据格式,这时用变量就不合适了
类型名如果过长的话可以考虑使用auto关键字
注释统一使用 // ,不要通过注释禁用代码,擅用git,不要为易懂的代码写注释
写完代码后记得format,每个项目最好都有统一的.clang_format文件
使用C++的string替代C语言风格的char*
尽量使用STL标准库的容器而不是C语言风格的数组,数组的越界访问之类当时是不会报错的,反而可能弄脏堆栈信息,导致奇奇怪怪难以排查的bug
可以更多的使用模板元编程,尽量多的使用constexpr等编译期计算,编译器是我们的好搭档,个人认为模板元编程以后会是C++的主流技术
可以考虑更多的使用异常处理方式,而不是C语言风格的errno错误码等
常用大括号控制生命周期,能提前结束的就可以提前结束
注意memcpy和memset的使用,仅适用于POD结构
理解内存对齐,可以使结构体更小,也可以使访问速度更快
基类析构函数一定要加virtual修饰
谨慎使用多继承,可能导致菱形继承
全局变量不要有任何依赖关系
这里我只列出来两本学习C++的书,可以按顺序阅读,我相信读完并理解这两本书的内容可以算是入门C++啦~
书
《C++ Primer Plus》 注意是Plus!
《Effective Modern C++》
视频
有喜欢看书的也有喜欢看视频的,这里再推荐两个比较好的C++视频教程:
①清华大学郑莉教授的视频,通俗易懂
https://www.bilibili.com/video/BV1QE41147RT?from=search&seid=8449657388898463710
②还有一个是技术交流群群友推荐,也是清华大学的课程
https://www.bilibili.com/video/BV1yk4y1d7ns?p=1
③喜欢英文的同学可以看看MIT或斯坦福的C++课程
具体是看书快还是看视频快,因人而异。适合自己的才是最实在、最好的。
再推荐两个比较好的C++学习网站:
最好的肯定是cppreference啦,不用多说,也可以看看cppfaq,里面的很多问题可以增长我们的见识,让我们对C++的理解也更加深刻。
http://www.sunistudio.com/cppfaq/
C++学习资料免费获取方法:关注程序喵大人,后台回复“程序喵”即可免费获取40万字C++进阶独家学习资料。