
尽管 Python 简单易用,但在许多对实时性有较高要求的场景中,仍然需要使用到C++,例如在ROS系统开发、边缘设备部署,仍需依赖 C++。因此,本文主要作为入门和学习C++过程中的笔记博客,也希望对同样想学习C++的同学有所帮助。👓
我是跟着Cherno大佬的视频开始学习C++,他的视频有趣且非常nice!Cherno-b站视频
除了视频以外,结合以下这些笔记内容进行了进一步整合和思考:
虽然C++开发有Visual Studio这款强大的IDE,但是由于本人此前深度使用Vscode,并且环境配置起来也相对比较简单,因此选择了Vscode+wsl2的方式进行C++环境的配置,并且最终使用SSH连接到wsl2中,使用起来也非常方便。
环境配置教程:wsl2+Vscode中配置C++开发环境
几个预编译指令:
● #include <XX>:实际上就是将后面文件的内容粘贴到文件中。例如:
假设定义一个x.h文件,里面内容只有一个},运行下面代码不会报错:

● #define XX YY 宏替换,就是将XX 替换成YY
● #if 表达式
.....
#endif 满足条件时才进行编译
const int MAX_AGE = 10;
const int*age = new int; // 同 int const* age = new int; 功能一致
age = &MAX_AGE;
cout << *age << endl; //输出结果10
*age = 10 //不允许,会报错!!
// 原因:const int* age 中的 const 修饰的是 int 类型的值,
// 而不是指针本身,所以指针指向的值不能被修改,指针所指的地址可以改变
// ——————————————————————————————————————————————————————————————
const int MAX_AGE = 10;
int* const age = new int;
*age = MAX_AGE;
cout << *age << endl; //输出结果10
age = MAX_AGE; //会报错const ,可以对m_X这个重新赋值;反之,则报错:
如果类中没有在函数的后面加上 const ,声明的静态类无法调用这个方法,反之则可以。

mutable,否则在函数中无法修改这个变量的值。(虽然可以在 GetX 后面去掉 const 实现,但是这样的话又回导致e.GetX()这个方法报错!)——mutable使用的大部分情况
new关键字,记住必须使用delete删除变量,释放内存。int* a = new int[23];
char *d = new char;
delete[] a; //使用new [] 就用 delete[] 删除
delete d;class Entity
{
private:
int m_age;
string m_X;
public:
Entity(int age) : m_age(age), m_X("good")
{
}
Entity(string m_name)
{
m_X = m_name;
m_age = 1;
}
const string GetName() {return m_X;}
};
// 显式调用:
Entity x(1);
Entity y(string("hello"));
// 隐式调用
Entity x = 1;
Entity y = string("hello");
// 如果在上面Entity的类中构造函数前加上 explicit,则无法使用隐式调用这种class Entity
{
public:
float x, y;
Entity(float x,float y)
{
this->x = x; //可以使用这种方式给x赋值
this->y = y;
}
};● 当类型名比较长时就适合用auto,其余情况还是不要用auto,因为会导致代码可读性变差。
● 注意:auto不处理引用,所以不要漏掉&而造成一次复制
int main()
{
std::vector<std::string> strings;
strings.push_back("hello");
strings.push_back("world");
for (std::vector<std::string>::iterator it = strings.begin(); it!=strings.end(); ++it){
std::cout << *it << std::endl;
}
// 使用auto关键字简化代码
for (auto it = strings.begin(); it!=strings.end(); ++it){
std::cout << *it << std::endl;
}
}#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
class Device{};
class DeviceManager{
private:
std::unordered_map<std::string, std::vector<Device*>> m_Devices;
public:
const std::unordered_map<std::string, std::vector<Device*>>& GetDevices() const{
return m_Devices;
}
};
int main()
{
DeviceManager dm;
// const std::unordered_map<std::string, std::vector<Device*>>& devices = dm.GetDevices();
// 简化代码
// 使用using声明
// using DeviceManager = std::unordered_map<std::string, std::vector<Device*>>;
// const DeviceManager& devices = dm.GetDevices();
// 或者使用typedef
// typedef std::unordered_map<std::string, std::vector<Device*>> DeviceMap;
// const DeviceMap &devices = dm.GetDevices();
// 或者使用auto
const auto &devices = dm.GetDevices(); // 注意auto不处理引用,所以不要漏掉&而造成一次复制
// auto devices = dm.GetDevices(); # 会复制一次
}数据类型 数组名常量表达式
// First
int example[5];
example[2] = 10;
// Second
int* xx = example;
*(xx + 2) = 20; //将指针 xx 移动2个位置(指向 example[2]),然后解引用该位置,将其值设置为 20
// Third
*(int*)((char*)xx +8) = 30;
// 首先将指针 xx 转换为 char* 类型。这意味着指针现在以字节(char)为单位进行指针算术。
// 然后,将指针 xx 增加8个字节(sizeof(int) 通常是4个字节,因此移动8个字节相当于移动2个 int 的位置,即 example[2] 的位置)
// 最后,将这个 char* 指针转换回 int* 指针,然后解引用,并将该位置的值设置为 30。这再次修改了 example[2] 的值#include <vector> vector<数组中元素类型> 标识符
struct vertex
{
public:
float x, y;
};
vector<vertex> vv;
vv.push_back({1,2}); //在数组的最后添加一个数据
vv.push_back({5,6});
vv.erase(vv.begin()+1); //删除第二个元素// 使用std::arrat
std::array<int, 5> data;
data[0] = 1;
data[4] = 2;
// C风格的普通数组
int dataOld[5];
dataOld[0] = 1;● 静态数组和普通数组的异同
○ 相同:都是在栈上分配,不像std::vector实在堆上分配的
○ 不同:std::array有边界检查(仅在debug模式下)
● 建议开始使用std::array,因为增加了一层调试,而且没有性能成本。
''。字符串使用""数据类型标识符 *指针变量名;
int i = 100;
int* ptr = &i; //&表示获取变量的地址 但是注意:int& ref = i; 表示引用
*ptr = 5; // 这是一个解引用操作符,用于访问指针 ptr 所指向的内存地址中存储的值。
void HelloWorld(){
std::cout<<"Hello World"<<std::endl;
}
int main()
{
void (*function)();
function = HelloWorld;
function();
function();
// 使用auto
auto fun= HelloWorld;
fun();
fun();
}
/*
Hello World
Hello World
Hello World
Hello World
*/void HelloWorld(int a)
{
std::cout << "HelloWorld:" << a << std::endl;
}
int main()
{
typedef void(*HelloWorldFunction)(int);
HelloWorldFunction function = HelloWorld;
function(3);
}
// 输出:HelloWorld:3#include <iostream>
#include <vector>
void PrintValue(int value){
std::cout << value << std::endl;
}
void ForEach(const std::vector<int>& values, void (*func)(int)){
for (int value: values){
func(value);
}
}
int main()
{
std::vector<int> values{1, 2, 3};
ForEach(values, PrintValue);
}
/*
1
2
3
*/数据类型 & 表达式;
void Increment(int value)
{
value++;
}
void Increment1(int* value)
{
(*value)++;
}
void Increment2(int& value)
{
value++;
}
int main()
{
int num =1;
Increment(num);
cout << "num is: " << num << endl;
Increment1(&num);
cout << "num iss:" << num << endl;
Increment2(num);
cout << "num isss: " << num << endl;
}
// 输出:
// num is: 1
// num iss:2
// num isss: 3enum 枚举类型名 {标识符列表}
● public:对外可见,对内可见;
● private:对外不可见,对内可见;
● protected:对外不可见,对内可见,且对派生类是可见的。
// public:类的外部也可以访问这个变量
class Father{
public:
int x,y;
public:
Father()
{
x = 10086;
}
};
class Son:public Father
{
public:
Son()
{
x = 10000;
}
};
int main()
{
Father f;
Son s;
f.x = 1;
s.x = 2;
}使用private声明后在子类中访问会报错,外部访问更会报错;

// 实现两个实例化的类进行相加:
// 法一:定义一个相加函数
struct Vector2
{
float x, y;
Vector2(float x, float y) : x(x), y(y){}
Vector2 Add(const Vector2& other) const
{
return Vector2(x + other.x, y + other.y);
};
};
Vector2 position(4.0f, 1.0f);
Vector2 speed(5.0f, 3.0f);
Vector2 result = position.Add(speed);
// 法二:重载运算符
struct Vector2
{
float x, y;
Vector2(float x, float y) : x(x), y(y){}
Vector2 operator+(const Vector2&other)
{
return Vector2 (x + other.x, y+other.y);
};
};
Vector2 result = position + speed;.XX 调用函数

class Qaq
{
public:
float x, y;
void Print()
{
cout << x << ", " << y << endl;
}
};
Qaq q;
q.Print(); //输出: -1.01376e+34, 4.59163e-41
// 现在若想要进行初始化,可以声明一个init函数,但是必须在实例化后调用这个函数才能初始化;
// 如果通过构造函数的方式实现就可以自动初始化。
// 添加构造函数
class Qaq
{
public:
float x, y;
Qaq()
{,
x = 1.0f;
y = 2.0f;
}
void Print()
{
cout << x << ", " << y << endl;
}
};
int main()
{
Qaq q;
q.Print();
} //输出: 1, 2在构造函数进行初始化类成员时有两种方法:
1️⃣. 常规方法:在构造函数的内部给变量赋值
class Entity
{
private:
string m_X;
public:
Entity()
{
m_X = "haha";
}
Entity(string m_name)
{
m_X = m_name;
}
const string GetName() {return m_X;}
};
int main()
{
Entity e1;
cout << e1.GetName() << endl;
Entity e2("wowo");
cout << e2.GetName() << endl;
}
/*
输出:
haha
wowo
*/2️⃣. 在构造函数后面使用这种形式:❤(优先选择这种方式去初始化,可读性和逻辑性)
函数名 : 参数名(初始化的值)
class Entity
{
private:
int m_age;
string m_X;
public:
Entity() : m_age(23), m_X("good") // 变量的初始化顺序 和 定义类成员的顺序一致
{
}
Entity(string m_name)
{
m_X = m_name;
}
const string GetName() {return m_X;}
};
在类对象的复制过程中,实际上调用了默认的拷贝构造函数,也可以自定义拷贝构造函数:
struct vector
{
float x, y;
vector(float m, float n)
: x(m), y(n) {}
vector(const vector &v)
{
x = v.x;
y = v.y;
cout << "Copy constructor called" << endl;
}
};
int main()
{
vector a = {2,3};
vector b =a;
}
// 终端输出:Copy constructor called#include <iostream>
#include <string.h>
using namespace std;
class String
{
private:
char* m_Buffer; //指向字符缓冲区
unsigned int m_Size; //保存string的大小
public:
String(const char* string)
{
m_Size = strlen(string);
m_Buffer = new char[m_Size];
// 把这个指针复制到实际的缓冲区,这样缓冲区就会被我们的字符填充
// 法一:
// for (int i = 0; i < m_Size; i++)
// m_Buffer[i] = string[i];
// 法二:更简洁
memcpy(m_Buffer, string, m_Size);
}
friend ostream &operator<<(ostream &stream, const String &string);
};
// 重载输出运算符,将 `String` 对象输出到流 `std::ostream` 中。
ostream& operator<<(ostream& stream, const String& string)
{
stream << string.m_Buffer;
return stream;
}
int main()
{
String string = "Cherno";
cout << string << endl;
// cin.get();
}上面代码存在几个问题:
1. 没有空终止字符,会导致出现随机字符

修改:
m_Buffer=newcharm_Size+1;//增加空终止符的位置 m_Bufferm_Size=0;//添加空终止符
2. 内存泄漏:分配的new char并没有delete
修改:
~String() { delete[] m_Buffer; }
3. 复制出错
// 如果对其进行复制,会出现以下错误:
String string = "Cherno";
String second = string;
cout << string << endl;
cout << second << endl;
出错原因:运行String second = string;时 C++自动为我们做的是它将所有类成员变量,而这些成员变量组成了类(实例的内存空间),它是由一个 char*和一个 unsigned int 组成的,它将这些值复制到了一个新的内存地址里面,这个新的内存地址包含了这个 second 字符串。
现在问题来了,内存中有两个 String,因为它们直接进行了复制,这种复制被称为shallow copy(浅拷贝)。它所做的是复制这个 char,内存中的两个 String 对象有相同的 char的值,换句话说就是有相同的内存地址。这个 m_Buffer 的内存地址,对于这两个 String 对象来说是相同的,所以程序会崩溃的原因是当我们到达作用域的尽头时,这两个 String 都被销毁了,析构函数会被调用,然后执行delete[] m_Buffer两次,程序试图两次释放同一个内存块。这就是为什么程序会崩溃——因为内存已经释放了,不再是我们的了,我们无法再次释放它。

如果修改second的某个值:second[2] = 'a';(需要设置操作符[]重载),最后会出现:


#include <iostream>
#include <string.h>
using namespace std;
class String
{
private:
char* m_Buffer; //指向字符缓冲区
unsigned int m_Size; //保存string的大小
public:
String(const char* string)
{
m_Size = strlen(string);
m_Buffer = new char[m_Size + 1];
m_Buffer[m_Size] = 0;
// 把这个指针复制到实际的缓冲区,这样缓冲区就会被我们的字符填充
// 法一:
// for (int i = 0; i < m_Size; i++)
// m_Buffer[i] = string[i];
// 法二:更简洁
memcpy(m_Buffer, string, m_Size);
}
String(const String& other) : m_Size(other.m_Size)
{
m_Buffer = new char[m_Size + 1];
memcpy(m_Buffer, other.m_Buffer, m_Size + 1);
}
~String()
{
delete[] m_Buffer;
}
char &operator[](unsigned int index)
{
return m_Buffer[index];
}
friend ostream &operator<<(ostream &stream, const String &string);
};
// 重载输出运算符,将 `String` 对象输出到流 `std::ostream` 中。
ostream& operator<<(ostream& stream, const String& string)
{
stream << string.m_Buffer;
return stream;
}
int main()
{
String string = "Cherno";
String second = string;
second[2] = 'a';
cout << string << endl;
cout << second << endl;
cin.get();
}cherno建议:在基础使用中用 const 引用更好。 总是要用 const 引用传递对象,因为你可以在你写的的函数的内部决定是否要复制,但是你没有理由到处复制,会拖慢你的程序。 重要的事情说三遍,当你传递字符串的时候,不管这个字符串是你自己的 String 类还是标准库里的 String(std::string),总是要通过 const 引用来传递。
struct vertex
{
float x, y;
vertex(float x, float y) : x(x), y(y) {}
vertex(const vertex& other) : x(other.x), y(other.y)
{
cout << "Copied!." << endl;
}
};
int main()
{
vector<vertex> vv;
vv.push_back(vertex(1.0, 2.0));
vv.push_back(vertex(3.0, 4.0));
}
// 此时进行了3次复制
/*输出:
Copied!.
Copied!.
Copied!.
*/
// 如果在增加一个元素:
vv.push_back(vertex(5.0, 6.0));
// 此时进行了6次复制
/*输出:
Copied!.
Copied!.
Copied!.
Copied!.
Copied!.
Copied!.
*/vertices.reserve(3);
此时节省一般,只复制3次,如果不用reserve的话复制次数会指数倍地增长vertices.reserve(3);
vertices.emplace_back(1, 2, 3);
vertices.emplace_back(4, 5, 6);
vertices.emplace_back(7, 8, 9);
// 此时一个copies都没有了emplace_back ,表示不是传递我们已经构建的 vertex 对象,而是只是传递了构造函数的参数列表~类名标识符
class Qaq
{
public:
float x, y;
Qaq()
{
x = 1.0f;
y = 2.0f;
}
~Qaq()
{
cout << "Destroyed Qaq" << endl;
};
void Print()
{
cout << x << ", " << y << endl;
}
};
int main()
{
Qaq q;
q.Print();
}
//输出:1, 2
// Destroyed Qaqclass 类名 : 继承方式 继承方式有3种:public、protected、private
class Paper
{
public:
string GetName() { return "Paper"; }
};
class Cver : public Paper
{
private:
string m_Name;
public:
Cver(const string& name) : m_Name(name) {}
string GetName() { return m_Name; }
};
int main()
{
Paper* w = new Paper();
cout << w->GetName() << endl;
Cver* p = new Cver("Cver");
cout << p->GetName() << endl;
Paper* qq = p;
cout << qq->GetName() << endl;
}
/* 输出:
Paper
Cver
Paper
*/
//声明虚函数后
class Paper
{
public:
virtual string GetName() { return "Paper"; }
};
class Cver : public Paper
{
private:
string m_Name;
public:
Cver(const string& name) : m_Name(name) {}
string GetName() { return m_Name; }
};
int main()
{
Paper* w = new Paper();
cout << w->GetName() << endl;
Cver* p = new Cver("Cver");
cout << p->GetName() << endl;
Paper* qq = p;
cout << qq->GetName() << endl;
}
int main()
{
Paper* w = new Paper();
cout << w->GetName() << endl;
Cver* p = new Cver("Cver");
cout << p->GetName() << endl;
Paper* qq = p;
cout << qq->GetName() << endl;
}
/* 输出:
Paper
Cver
Cver
*/virual 类型 函数名(参数列表)=0;
// 比如我想要实现一个可以打印类名的功能
// 根据程序“由下而上”的思想,首先实现一个打印功能 Print()
void Print(Print_ClassName* obj)
{
cout << obj -> GetClassName() << endl;
};
// Print_ClassName是需要定义的类,其中的获取类名的函数GetClassName()
// 就可以用pure virtual function实现,然后由子类再去实现这个函数
// 定义Print_ClassName,留出GetClassName这个接口
class Print_ClassName
{
public:
virtual string GetClassName() = 0;
};
// 定义AA这个类,并且实现纯函数
class AA: public Print_ClassName
{
public:
string GetClassName() override {return "AA"; }
};
int main()
{
Print(new AA());
}
// 输出:AAstruct Entity
{
int x, y;
void Print()
{
cout << x << ", " << y << endl;
}
};
int main()
{
Entity e;
e.x = 2;
e.y = 3;
Entity e1;
e1.x = 5,
e1.y = 7;
e.Print();
e1.Print();
}
//输出结果:
// 2, 3
// 5, 7
//声明静态成员
struct Entity
{
static int x, y;
void Print()
{
cout << x << ", " << y << endl;
}
};
int Entity::x; //需要在类的外部对静态数据成员进行初始化
int Entity::y;
int main()
{
Entity e;
e.x = 2;
e.y = 3;
Entity e1;
e1.x = 5,
e1.y = 7;
e.Print();
e1.Print();
}
//输出结果:
//5, 7
//5, 7new关键字// 栈上定义int array
int value = 5;
int arary[5];
// 堆上定义int array
int* value1 = new int;
*value1 = 10;
int * array1 = new int[5];
class Entity
{
public:
Entity()
{
cout << "created Entity!" << endl;
}
~Entity()
{
cout << "Destroyed Entity!" << endl;
}
};
int main()
{
Entity e; //在栈上创建的
Entity* e = new Entity; //在堆上创建的
}new关键字实际上调用了一个叫做malloc的函数的缩写,这样做通常会调用底层操作系统或平台的特定函数,这将在堆上为你分配内存。当你使用malloc请求堆内存时,它可以浏览空闲列表,找到一块符合大小要求的内存块,然后返回你一个它的指针,并记录分配的大小和它现在是否被分配的情况#include <iostream>
#include <vector>
struct str_int
{
int index;
char value;
};
std::vector<str_int> get_string_index(std::string &str){
std::vector<str_int> result;
for(int i=0; i<str.size(); i++){
str_int temp; // temp 存储了一个int类型和char类型
temp.index = i;
temp.value = str[i];
result.push_back(temp);
}
return result;
};
int main(){
std::string str = "hello";
std::vector<str_int> result = get_string_index(str);
for(auto i: result){
std::cout << i.index << " " << i.value << std::endl;
}
}
//输出:
/*
0 h
1 e
2 l
3 l
4 o
*/2️⃣tuple和pair
● pair:使用.first 和.second 访问元素
#include <iostream>
#include <vector>
std::vector<std::pair<int, char>> get_string_index(std::string &str){
std::vector<std::pair<int, char>> result;
for(int i=0; i<str.size(); i++){
std::pair<int, char> temp; // temp 存储了一个int类型和char类型
temp.first = i;
temp.second = str[i];
result.push_back(temp);
}
return result;
};
int main(){
std::string str = "hello world";
std::vector<std::pair<int, char>> result = get_string_index(str);
for(auto i: result){
std::cout << i.first << " " << i.second<< std::endl;
}
}● tuple:使用get<i>(name) 访问元素第i个元素
#include <iostream>
#include <vector>
#include <tuple>
// struct str_int
// {
// int index;
// char value;
// };
std::vector<std::tuple<int, char>> get_string_index(std::string &str){
std::vector<std::tuple<int, char>> result;
for(int i=0; i<str.size(); i++){
std::tuple<int, char> temp; // temp 存储了一个int类型和char类型
std::get<0>(temp) = i;
std::get<1>(temp) = str[i];
result.push_back(temp);
}
return result;
};
int main(){
std::string str = "hello";
std::vector<std::tuple<int, char>> result = get_string_index(str);
for(auto i: result){
std::cout << std::get<0>(i) << " " << std::get<1>(i) << std::endl;
}
}3️⃣指针和引用
#include <iostream>
void returnWithReference(std::string& str, int& num)
{
str = "Hello";
num = 42;
}
int main()
{
std::string str;
int num;
returnWithReference(str, num);
std::cout << str << ", " << num << std::endl;
return 0;
}
// 输出:
/*
Hello, 42
*/template <typename 形参名, typename 形参名..> typename 可以使用class 替代
// 实现一个打印函数,由于输入类型不同,需要多次重载
#include <iostream>
#include <string>
void Print(int value)
{
std::cout << value << std::endl;
}
void Print(std::string value) // 一次函数重载
{
std::cout << value << std::endl;
}
int main()
{
Print(5);
Print("Hello");
Print(4.3f); // 这里要是再想打印一个float类型还得再重载一次
std::cin.get();
}此时使用模版简化:
template<typename T>
void Print(T value)
{
std::cout << value << std::endl;
}也可以使用整数作为模版参数:
template <int N>
class Array
{
private:
int m_Array[N];
public:
int GetSize() const { return N; }
};
int main()
{
Array<5> array;
std::cout << array.GetSize() << std::endl; // 5
std::cin.get();
}写一个可以自动创建的类
template <typename T, int N>
class Array
{
private:
T m_Array[N];
public:
int GetSize() const { return N; }
};
int main(){
// 调用
Array<int, 5> array0;
Array<std::string, 50> array1;
}● 总结:
○ 模板类型参数: template<typename T> 此时可以像使用任何其他类型一样使用T。
○ 非类型模板参数:允许我们在模板中使用常量值作为参数。用于在模板定义中指定一个常量值,而不是一个数据类型。非类型模板参数可以是整数、枚举、指针或引用类型。例如:template<int N>
○ 可以使用模板来做一些很好的事,比如日志系统记录每一种可能的类型,但是不要将模板写的复杂,因为后续就很难知道到底发生了什么。
使用#define定义,例如 #define PI 3.14159265
#define LOG(x) std::cout << x << std::endl;
int main(){
LOG("Hello, World!")
}
// 或者
#define LOG(x) std::cout << x << std::endl
int main(){
LOG("Hello, World!");
}
\来表示换行,注意反斜杠后面不要有空格,不然就变成对空格转义了。#define LOG(x) std::cout << x << std::endl;
#define MAIN int main() \
{\
LOG("Hello, World!")\
}
MAINcapture list -> return type { function body }
capture list是捕获列表,用于指定 Lambda表达式可以访问的外部变量,以及是按值还是按引用的方式访问。捕获列表可以为空,表示不访问任何外部变量。parameter list是参数列表,用于表示 Lambda表达式的参数,可以为空,表示没有参数return type是返回值类型,用于指定 Lambda表达式的返回值类型,可以省略function body是函数体,用于表示 Lambda表达式的具体逻辑 =,传递所有变量,通过值传递;&传递所有变量,通过引用传递
std::find_if 函数在 values 容器中查找满足 lambda 表达式的条件的第一个元素。
现在使用lambda表达式找到第一个满足条件的元素:#include <algorithm>
auto it = std::find_if(values.begin(), values.end(), [](int value) {return value > 3; });
std::cout << *it << std::endl;
# 相当于以下的功能:
void ForEach(const std::vector<int>& values,const std::function<void(int)>& func)
{
for (int value : values)
if(value > 3) // 添加bool判断
func(value);
}原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。