目录
关键字
-
sizeof()
sizeof 是 C 语言中的运算符,用来计算一个类型/对象的所占用的内存(字节)大小
- 指针的大小永远是固定的,取决于处理器位数,32位就是 4 字节,64位就是 8 字节
- 数组作为函数参数时会退化为指针,大小要按指针的计算
- struct 结构体要考虑字节对齐
- 字符串数组要算上末尾的 '\0'
-
using
using 是 C++11 中的一个关键字,用于定义类型别名(type alias) using Request = nav_interfaces::srv::AvoidObstacleControl_Request;
-
explicit
用在构造函数前,限制使用特定的方式创建类对象,保证系统安全
-
const
-
修饰成员变量,不可修改
-
修饰成员函数为常函数:
-
成员函数后加const后我们称这个函数为常函数
-
常函数不可以修改成员属性
-
-
枚举
枚举可以看作是一个类,声明一个枚举对象
enum <类型名> {<枚举常量表>};
enum week {Sun, Mon, Tue, Wed, Thu, Fri, Sat}; // 定义枚举类型week
week week1,week2;//声明该枚举类型的变量
//相关操作
week1 = Mon;
week2 = Thu;
//枚举是可以比较的
if(week1 == week2) cout<<"equal";
数字与枚举变量比较
enum Color {
RED,
GREEN,
BLUE
};
int main() {
int num = 1;
Color color = GREEN;
if (num == GREEN) {
std::cout << "Equal" << std::endl;
} else {
std::cout << "Not equal" << std::endl;
}
if (color == GREEN) {
std::cout << "Equal" << std::endl;
} else {
std::cout << "Not equal" << std::endl;
}
return 0;
}
switch case
函数
-
末尾加const的作用表示该成员函数是一个常量成员函数。
-
常量成员函数是指在函数内部不允许修改对象的成员变量
-
只能调用带const的函数,不允许调用非常量成员函数
-
-
Callback callback
常用函数
-
std::ref
主要用于传递对象的引用,例如作为函数的参数或线程的参数等。在这些情况下,如果直接传递对象而不是引用,会导致对象被复制一份,从而增加了程序的开销。使用
std::ref
可以避免这种开销,同时还可以保证传递的是引用而不是对象的副本。 -
std::bind
函数模板的声明定义在<functional>
头文件中,其函数签名如下:template<class F, class... Args> /*unspecified*/ bind(F&& f, Args&&... args);
其中,
F
表示要绑定的成员函数或函数对象的类型,Args...
表示成员函数或函数对象的参数类型,f
表示要绑定的成员函数或函数对象,args...
表示要绑定的成员函数或函数对象的参数。std::bind
函数模板返回一个可调用对象,该可调用对象可以使用指定的成员函数或函数对象来调用成员函数,并将占位符所代表的参数传递给成员函数或函数对象。需要注意的是,
std::bind
函数模板支持任意类型的参数,包括左值引用和右值引用。同时,也支持将占位符_1
、_2
、_3
等作为参数,用于表示传递给成员函数或函数对象的参数。例子
std::bind(&MotionService::localObstaclesMapCallBack, this, _1, _2, _3, _4)
-
td::bind
函数接受一个成员函数指针作为第一个参数,该成员函数指针指向要绑定的成员函数。 -
接下来的参数是要绑定的对象指针,即成员函数所属的对象。在这个例子中,使用
this
指针作为对象指针,即将成员函数localObstaclesMapCallBack
绑定到当前对象上。 -
接下来的四个参数
_1
、_2
、_3
和_4
是占位符,用于表示传递给回调函数的参数。在这个例子中,成员函数localObstaclesMapCallBack
接受四个参数,因此使用了四个占位符。
-
代码块
int add(int a, int b) {
int sum = a + b;
{
int x = 10;
sum += x;
}
return sum;
}
需要注意的是,代码块中定义的变量只在代码块内部可见,超出代码块的范围后将无法访问。在上面的例子中,变量 x
只在代码块中可见,函数的其他部分无法访问它。这种做法可以有效地限制变量的作用域,提高程序的可读性和可维护性。
指针
- 指针的本质就是地址,
- 数组也是指针,数组变量第一个元素的地址
- 传递指针给函数,会改变变量的值
智能指针
- 于是花了点时间把每个智能指针手写了一遍,在之后的面试中回答的也让面试官比较满意
C++11 引入了 std::unique_ptr
和 std::shared_ptr
两种智能指针,它们可以自动管理动态分配的内存,确保每块内存只被释放一次
-
std::unique_ptr<T>
:独占资源所有权的指针。 -
std::shared_ptr<T>
:共享资源所有权的指针 -
使用 std::unique_ptr 自动管理内存。
{
std::unique_ptr<int> uptr = std::make_unique<int>(200);
//...
// 离开 uptr 的作用域的时候自动释放内存
}
引用
引用本质:引用的本质在c++内部实现是一个指针常量,引用一旦被初始化之后就不能更改,不能更改指向。
把引用作为参数,在函数内改变变量的值,变量会跟着改变
引用很容易与指针混淆,它们之间有三个主要的不同:
- 不存在空引用。引用必须连接到一块合法的内存。
- 一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。
- 引用必须在创建时被初始化。指针可以在任何时间被初始化。
传参传递引用的好处
- 使用引用传递参数时,可以避免拷贝参数值的开销,提高程序的性能
类与对象
C++中对象就是对象本身
- 成员变量声明为private,访问,修改成员变量通过函数,这就是封装
构造函数与析构函数
- 类的构造函数是一种特殊的函数,在创建一个新的对象时调用。类的析构函数也是一种特殊的函数,在删除所创建的对象时调用。
this指针
每个对象都有一个特殊的指针 this,它指向对象本身。
静态成员
我们可以使用 static 关键字来把类成员定义为静态的。当我们声明类的成员为静态时,这意味着无论创建多少个类的对象,静态成员都只有一个副本。
静态成员变量
静态成员在类的所有对象中是共享的。在创建第一个对象时,所有的静态数据会被初始化,且只会被初始化一次,静态变量的值可以被更改
- 可以通过类+范围解析运算符 ::+静态变量,访问
静态成员函数
静态成员函数与普通成员函数的区别:
- 静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)。
- 普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针。
继承与多态与抽象类
-
继承是可以多继承的
-
虚函数 是在基类中使用关键字 virtual 声明的函数,在程序中任意点可以根据所调用的对象类型来选择调用的函数
-
在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数
virtual int area() = 0;
-
定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
#include <iostream>
using namespace std;
// 基类
class Shape
{
public:
// 提供接口框架的纯虚函数
virtual int getArea() = 0;
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
// 派生类
class Rectangle: public Shape
{
public:
int getArea()
{
return (width * height);
}
};
class Triangle: public Shape
{
public:
int getArea()
{
return (width * height)/2;
}
};
int main(void)
{
Rectangle Rect;
Triangle Tri;
Rect.setWidth(5);
Rect.setHeight(7);
// 输出对象的面积
cout << "Total Rectangle area: " << Rect.getArea() << endl;
Tri.setWidth(5);
Tri.setHeight(7);
// 输出对象的面积
cout << "Total Triangle area: " << Tri.getArea() << endl;
return 0;
}
Total Rectangle area: 35
Total Triangle area: 17
动态内存
double* pvalue = NULL; // 初始化为 null 的指针
pvalue = new double; // 为变量请求内存
- 在 C++ 中,您可以使用特殊的运算符为给定类型的变量在运行时分配堆内的内存,这会返回所分配的空间地址。这种运算符即 new 运算符。
- new 不只是动态分配了内存,它还创建了指针对象。
拷贝
浅拷贝
浅拷贝是指一个对象obj1的成员变量的值被简单地复制到另一个对象obj2中,而不涉及到对象所拥有的资源或状态。在 C++ 中,如果一个类没有重载赋值运算符 =
,那么默认的赋值运算符会执行浅拷贝。如果成员变量存在指针,那么obj1和obj2的成员变量会指向同一个指针即指向指向同一块内存地址
命名空间
它可作为附加信息来区分不同库中相同名称的函数、类、变量等。使用了命名空间即定义了上下文。本质上,命名空间就是定义了一个范围。
I/O文件和流
程序运行时,产生的数据都属于临时数据,程序一旦运行结束就会被释放。通过文件可以将数据持久化。C++中对文件进行操作需要包含头文件< Fstream>
数据类型 | 描述 |
---|---|
ofstream | 该数据类型表示输出文件流,用于创建文件并向文件写入信息。 |
ifstream | 该数据类型表示输入文件流,用于从文件读取信息。 |
fstream | 该数据类型通常表示文件流,且同时具有 ofstream 和 ifstream 两种功能,这意味着它可以创建文件,向文件写入信息,从文件读取信息。 |
写文件
- 包含头文件——#include< fstream>
- 创建流对象——ofstream ofs;
- 打开文件——ofs.open(“文件路径”,打开方式)
- 写数据——ofs<<“写入的数据”;
- 关闭文件——ofs.close();
读文件
- 包含头文件——#include< fstream>
- 创建流对象——ifstream ifs;
- 打开文件并判断文件是否打开成功——ifs.open(“文件路径”,打开方式);
- 读数据——四种方式读取
- 关闭文件——ifs.close();
异常处理
C++中定义的异常类:std::exception
捕获异常
try
{
// 保护代码
}catch( ExceptionName e1 )
{
// catch 块
}
抛出异常
if( b == 0 )
{
throw "Division by zero condition!";
}
模板
泛型编程的基础,在函数或类中,变量的类型不是特定的,可以是任何类型
函数模板
一般定义形式
template <typename type> ret-type func-name(parameter list)
{
// 函数的主体
}
类模板
template <class type> class class-name {
//类主体
}
STL
容器(Containers)
vector
vector<类型名> 变量名;
//迭代方式
第一种
//v.begin()返回v的首元素地址
vector<int>::iterator it=v.begin();
for (int i = 0; i < v.size(); i++)
{
cout<<it[i]<<" ";
}
第二种
for (vector<int>::iterator it=v.begin(); it!=v.end();it++)
{
cout<<*it<<" ";
}
第三种
for(auto x : a)
{
cout<<x<<" ";
}
函数
- push_back()
- pop_back()
- size()
- insert()
map
map<key_type, value_type>变量名
//常用方法
size() // 计算元素个数
empty() // 判断是否为空,空返回 true
clear() // 清空容器
erase() // 删除元素
find() // 查找元素
insert() // 插入元素
count() // 计算指定元素出现的次数
begin() // 返回迭代器头部
end() // 返回迭代器尾部
//遍历数据
map<int, string>::iterator iter; //定义迭代器 iter
for(iter = node.begin(); iter != node.end(); ++iter) {
cout<<"身份证号"<<iter->first<<"的人叫"<<iter->second<<endl;
}
unordered_map
算法(Algorithm)
迭代器(Iterators)
迭代器是指针
多线程
多线程与多进程
- 多进程并发
使用多进程并发是将一个应用程序划分为多个独立的进程(每个进程只有一个线程),这些独立的进程间可以互相通信,共同完成任务。由于操作系统对进程提供了大量的保护机制,以避免一个进程修改了另一个进程的数据,使用多进程比使用多线程更容易写出相对安全的代码。但是这也造就了多进程并发的两个缺点:
- 进程作为资源调度的独立单位,进程与进程之间无法共享资源。在进程间的通信,无论是使用信号、套接字,还是文件、管道等方式,其使用要么比较复杂,要么就是速度较慢或者两者兼而有之。
- 运行多个进程的开销很大,操作系统要分配很多的资源来对这些进程进行管理。
当多个进程并发完成同一个任务时,不可避免的是:操作同一个数据和进程间的相互通信,上述的两个缺点也就决定了多进程的并发并不是一个好的选择。所以就引入了多线程的并发。
- 多线程并发
多线程并发指的是在同一个进程中执行多个线程。
线程是轻量级的进程,每个线程可以独立的运行不同的指令序列,但是线程不独立的拥有资源,依赖于创建它的进程而存在。也就是说,同一进程中的多个线程共享相同的地址空间,可以访问进程中的大部分数据,指针和引用可以在线程间进行传递。这样,同一进程内的多个线程能够很方便的进行数据共享以及通信,也就比进程更适用于并发操作。
缺点:在多线程共享数据及通信时,就需要程序员做更多的工作以保证对共享数据段的操作是以预想的操作顺序进行的,并且要极力的避免死锁(deadlock)。
创建线程
//构造函数源码
template<typename _Callable, typename... _Args>
explicit
thread(_Callable&& __f, _Args&&... __args)
{}
-
_Callable&& __f:一个可调用对象可以是以下三个中的任何一个:
-
函数指针
-
函数对象
-
lambda 表达式
定义 callable 后,将其传递给 std::thread 构造函数 thread_object。
-
-
_Args&&... __args:可变参数模板,可调用对象所需参数
-
最后使用
join()
函数等待线程执行完毕
需要注意的是,该构造函数使用了完美转发(perfect forwarding)的技术,可以接受任意类型的函数对象和参数,并将它们转发给新线程中的函数对象。同时,由于该构造函数使用了模板和可变参数模板的技术,因此可以支持任意数量和类型的参数。
#include <iostream>
#include <thread>
using namespace std;
void thread_1()
{
cout<<"子线程1"<<endl;
}
void thread_2(int x)
{
cout<<"x:"<<x<<endl;
cout<<"子线程2"<<endl;
}
int main()
{
thread first ( thread_1); // 开启线程,调用:thread_1()
thread second (thread_2,100); // 开启线程,调用:thread_2(100)
//thread third(thread_2,3);//开启第3个线程,共享thread_2函数。
std::cout << "主线程\n";
first.join(); //必须说明添加线程的方式
second.join();
std::cout << "子线程结束.\n";//必须join完成
return 0;
}
如何传递引用
使用 std::ref()
#include <iostream>
#include <thread>
void threadCallback(int const & x)
{
int & y = const_cast<int &>(x);
y++;
std::cout << "在新线程中 x = " << x << std::endl;
}
int main()
{
int x = 9;
std::cout << "在主线程中:在新线程开始运行前的 x = " << x << std::endl;
std::thread threadObj(threadCallback, std::ref(x));
threadObj.join();
std::cout << "在主线程中:在新线程会和后的 x = " << x << std::endl;
return 0;
}
线程周期
线程休眠
#include <chrono>
#include <thread>
std::chrono::microseconds delayTime(m_plannerParam.shareMemoryStartDelayTime * 1000000);
// sleep 20s
std::this_thread::sleep_for(delayTime);
在该代码中,使用 std::chrono::microseconds
类型来表示微秒数,然后使用 std::this_thread::sleep_for
函数让当前线程休眠指定的时间。这种方式更加符合 C++11 标准,并且更加可读、可维护。
锁
当一个线程访问系统资源时,存在另外的线程在同时访问该资源,从而造成资源访问的混乱
std::lock_guard
std::lock_guard
是一个模版类,能够实现锁的 RAII 管理。其在内部包装 mutex
,并在构造函数中完成上锁,并将在其的析构函数中释放锁。
std::lock_guard的作用域
std::lock_guard
对象的作用域可以通过花括号{}
来设置- 如果在不使用花括号
{}
的情况下创建std::lock_guard
对象,那么它的作用域将是从创建std::lock_guard
对象的位置开始,一直到当前函数或代码块的末尾 - 创建即加锁,作用域结束自动析构并解锁,无需手工解锁
- 不能中途解锁,必须等作用域结束才解锁
#include <mutex>
std::mutex myMutex;
void myFunction()
{
std::lock_guard<std::mutex> myLock(myMutex); // 自动加锁
// 在这里执行线程安全的代码
} // 自动解锁
unique_lock
简单地讲,unique_lock 是 lock_guard 的升级加强版,它具有 lock_guard 的所有功能,同时又具有其他很多方法,使用起来更加灵活方便,能够应对更复杂的锁定需要。unique_lock的特点:
- 创建时可以不锁定(通过指定第二个参数为std::defer_lock),而在需要时再锁定
- 可以随时加锁解锁
- 作用域规则同 lock_grard,析构时自动释放锁
- 不可复制,可移动
- 条件变量需要该类型的锁作为参数(此时必须使用unique_lock)
条件变量
条件变量是一种【事件】( event ),其可被用于在两个或多个线程之间传递信号。一个或多个线程可以在等待条件变量时保持阻塞状态(睡眠,此时不占用 CPU 资源),当其他线程对条件变量发出信号时,这些等待对应条件变量的线程就会被唤醒。
C++ 11 的条件变量包含在 <condition_variable>
头文件中。条件变量需要和互斥锁配合使用。
std::condition_variable 的主要成员函数
wait()
wait()
会使当前线程进入阻塞状态,直到条件变量获得信号,该线程才会继续运行。
该函数会自动释放与之关联的互斥锁,阻塞所在线程,并将该线程加入到等待当前条件变量的线程列表中。当有其他线程调用当前条件变量的 notify_one()
或 notify_all()
成员函数时,该线程才会继续执行。但由于唤醒也可能是虚假唤醒,因此每次结束阻塞后都应立即重新检查自己所等待的条件是否满足。
该函数有一个回调函数作为自己的参数——这个回调函数的作用,就是用来检查本次唤醒是否为虚假唤醒。
当线程解除阻塞后,wait()
函数会重新对互斥量加锁,调用回调函数检查条件是否满足。若条件不满足,则该函数会以一次原子操作释放锁,然后阻塞当前线程,并将当前线程加入到等待当前条件变量的线程列表中。
notify_one()
若有多个线程在等待同一个条件变量,则 notify_one()
会只唤醒这些阻塞线程中的一个线程。
notify_all()
若有多个线程在等待同一个条件变量,则 notify_all()
会唤醒所有的线程。
三个线程轮流打印A B C
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include "hello.h"
std::mutex mtx;
std::condition_variable cv;
std::string flag = "A";
int i = 1;
void printA()
{
while (i < 100)
{
{
std::unique_lock<std::mutex> lck(mtx);
while (flag !="A")
{
cv.wait(lck);
}
std::cout << flag << i << std::endl;
flag = "B";
i++;
cv.notify_all();
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
void printB()
{
while (i < 100)
{
/* code */
{
std::unique_lock<std::mutex> lck(mtx);
while (flag !="B")
{
cv.wait(lck);
}
std::cout << flag << i << std::endl;
flag = "C";
i++;
cv.notify_all();
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
void printC()
{
while (i < 100)
{
/* code */
{
std::unique_lock<std::mutex> lck(mtx);
while (flag !="C")
{
cv.wait(lck);
}
std::cout << flag << i << std::endl;
flag = "A";
i++;
cv.notify_all();
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main()
{
hello();
std::thread t1(printA);
std::thread t2(printB);
std::thread t3(printC);
t1.join();
t2.join();
t3.join();
return 0;
}
消除魔法数字的方法
枚举
静态变量
static const int Max = 10;
宏定义
例子
twosum
#include<stdio.h>
#include<iostream>
#include<map>
#include<vector>
using namespace std;
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
map<int,int> a;//提供一对一的hash
vector<int> b(2,-1);//用来承载结果,初始化一个大小为2,值为-1的容器b
for(int i=0;i<nums.size();i++)
{
if(a.count(nums[i])>0)
{
b[0]=a[target-nums[i]];
b[1]=i;
break;
}
a[target-nums[i]]=i;//反过来放入map中,用来获取结果下标
}
return b;
};
};
int main()
{
vector<int> nums = {1,2,3,4,5,6,7,8,9};
int target = 3;
Solution solution;
vector<int> r = solution.twoSum(nums,target);
for(auto x:r)
{
cout<<x<<endl;
}
}
C++岗位发展方向
立足现在,才能有未来
- 选工作应该第一个要考虑行业。行业有兴衰,选错行业会比别人费很多劲儿。
- 切记不要过度追求代码技巧
- 决策规划
- **后端/服务端开发 **×
- 人工智能部署 √
- 客户端:
C++
写桌面软件开发 - 音视频/流媒体
- 游戏/图形学/AR,VR
- 嵌入式
- 量化/金融
- 策略开发?
编程心得
开始编写程序
- 入参
- 数据来源
- 定义所需变量,定义所需容器
- 初始化变量,初始化容器