【c++】0.C++笔记

1.DISALLOW_COPY_AND_ASSIGN

2.延时函数delay()、sleep()、usleep()

3.opencv在图片中绘图常使用的几个函数

4.opencv中 cv::Mat 与 cv::Rect 在一起使用的情况

5.imshow()显示图像帧时暂停键的用法

6.opencv播放视频 以及 设置任意键为暂停键

7.opencv旋转图片

8.const和constexpr相关知识

9.a.cc文件中调用另一个b.cc文件中的函数

10.视频编码格式与封装格式,opencv2.4.8不能读取h265编码格式的视频文件

11.当使用imshow显示每一帧画面时,出现卡顿的解决办法

12.cv::VideoWriter的使用,把图片写进video

13.C++11多线程join()和detach()的理解

14.获取鼠标动作进行相应处理

15.删除容器中指定元素

15.1 map 的insert和emplace方法

15.2 map的erase(iter)需注意,和vector不一样

16.sprintf的用法

17.std::shared_ptr、std::make_shared、 .get() 、.data()、void *p 的用法、裸指针

18.选择使用memcpy_s,strcpy_s还是选择strcpy,memcpy? memset()的用法

19.C++强制类型转换(dynamic_cast,static_cast, const_cast, reinterpret_cast)

20.编译proto的问题

21.比较大小时用减法越界的bug问题

22.摄像头焦距和视场角

23.RGB和BGR的转化,通道分离与合并

24.重写、覆盖、using、typedef

25.静态成员静态成员函数/变量、单例模式

26.proto相关用法

27.深拷贝、浅拷贝、拷贝构造函数 之间的关系

1.DISALLOW_COPY_AND_ASSIGN

有时候,进行类体设计时,会发现某个类的对象是独一无二的,没有完全相同的对象,也就是对该类对象做副本没有任何意义.

因此,需要限制编译器自动生动的拷贝构造函数和赋值构造函数.一般参用下面的宏定义的方式进行限制,代码如下:

// A macro to disallow the copy constructor and operator= functions 
// This should be used in the priavte:declarations for a class
#define    DISALLOW_COPY_AND_ASSIGN(TypeName) \
    TypeName(const TypeName&);                \
    TypeName& operator=(const TypeName&)

class Test {
public:
    Test(int t);
    ~Test();
private:
    DISALLOW_COPY_AND_ASSIGN(Test);
};

声明私有的拷贝构造函数和赋值构造函数,但不去定义实现它们,有三方面的作用:

1.声明了拷贝构造函数和赋this数,阻止了编译器暗自创建的专属版本.

2.声明了private,阻止了外this它们的调用.

3.不定义它们,可以保证成员函数和友元函数调用它们时,产生一个连接错误.

上述解决方法,面对在成员函数和友元函数企图拷贝对象时,会产生连接器错误

@zhu.hz的注释:

1)由于是在private下,外部调用不了,所以就不能进行copy和赋值操作,即不能进行 Test A(obj) 或 Test A=obj 的copy操作(拷贝构造),和 A=obj 或 A(obj) 的assign操作(拷贝赋值),【其中obj是先通过Test obj;得到的】。

(2)他与单例模式不是一个概念,单例模式只允许有一个实例,而这个可以允许有多个对象,但是多个对象都是不一样的,不允许进行拷贝构造和赋值构造,即不允许任何两个对象一样。

(3)拷贝构造是在构造对象时候就使用另一个对象对他进行赋值。拷贝赋值是在构造对象完成之后,再对他进行赋值操作。一个是在创建他这个对象的时候就赋值了,另一个是在对象构造完成之后才对他进行赋值。
     毕竟只有这两种方式可以对另一个对象进行赋值。这么赋值是为了初始化数据成员,而类本身的初始化数据可能不如用另一个对象的数据更合适。
     比如一个person类只有两个成员height和weight,两个数据成员在构造函数时初始化为0, 如果两个人身高体重都一样,就可以先创建其中一个对象后,直接拷贝构造或者拷贝赋值把先创建的对象赋值给另一个人,肯定比直接构造
     一个对象不进行赋值的初始化数据更好。这个例子太简单,实际中肯定不可能这么简单。

=================================================================

2.延时函数delay(),sleep(),usleep()

#include <time.h>       /* 调用时务必加上该头文件 */
// 自定义的一个延时函数delay()
void delay(int seconds) //  参数必须为整型,表示延时多少秒
{
    clock_t start = clock();
    clock_t lay = (clock_t)seconds * CLOCKS_PER_SEC;
    while ((clock()-start) < lay);
 }

void delay(double seconds) //  参数为双精度浮点型。这个函数是我修改的上面那个函数,重载一下。
 {
    double start = clock();
    double lay = (double)seconds * CLOCKS_PER_SEC;
    while ((clock()-start) < lay);
 }
 百度Apollo里面用的是延时毫秒usleep(unsigned int __useconds)和延时秒sleep(unsigned int __seconds)函数,他们的输入参数也都是整数,不能为浮点数,
 比如不能sleep(0.5)来表示延时0.5s。 但是当函数设定的计时器到期,或者接收到信号、程序发生中断都会导致程序继续执行。

=================================================================

3.opencv在图片中绘图常使用的几个函数

opencv中的(0,0)坐标是在图像的左上角。

一般会将 cv::getTextSize()cv::putText() 结合使用.

 cv::getTextSize()  //实际绘制文字之前,使用cv::getTextSize()接口先获取待绘制文本框的大小,以方便放置文本框。返回值为cv::Size。设返回值为size,可以通过size.width和size.height来获取文本框的宽和高.
 cv::putText()      //在图像上绘制文字
 
 cv::rectangle()    //在Mat上画矩形矩形框,如果要填充矩形,需要把thickness设为-1。详解看后面。
 cv::Rect           //定义一个矩形,有成员变量x,y,weight,height,如Rect rect1(256, 256, 128, 128),这样用是构造函数;
 cv::Point()        //定义一个2D的点:如Point point = Point(10, 8);或者 Point point;//创建一个2D点对象  point.x = 10;//初始化x坐标值  point.y = 8;//初始化y坐标值 
 cv::Scalar(255, 255, 0)    //一般作为值来设置颜色。他是一个由长度为4的数组作为元素构成的结构体,Scalar最多可以存储四个值,没有提供的值默认是0。
//  关于cv::Scalar的更详细资料可以参考 https://blog.csdn.net/liuweiyuxiang/article/details/76929534
 
cv::Size cv::getTextSize(
		const string& text,     // 待绘制的文字
		int fontFace,           // 字体类型 (如 cv::FONT_HERSHEY_PLAIN, cv::FONT_HERSHEY_SIMPLEX)
		double fontScale,       // 尺寸因子,值越大文字越大
		int thickness,          // 线条宽度
		int* baseLine
	);

void cv::putText(
		cv::Mat& img,       // 待绘制的图像
		const string& text, // 待绘制的文字
		cv::Point origin,   // 文本框的左下角坐标(x,y),记住是左下角,不是左上角,要和cv::rectangle()的左上角区分开
		int fontFace,       // 字体类型 (如cv::FONT_HERSHEY_PLAIN, cv::FONT_HERSHEY_SIMPLEX)
		double fontScale,   // 尺寸因子,值越大文字越大
		cv::Scalar color,   // 线条的颜色(RGB)
		int thickness = 1,  // 线条宽度
		int lineType = 8,   // 线型(4邻域或8邻域,默认8邻域)
		bool bottomLeftOrigin = false // true='origin at lower left'
	);

对于在图片中画矩形框的函数,C++中opencv对void cv::rectangle()重载了两种用法:
// pt1 矩形的一个顶点; pt2 矩形对角线上的另一个顶点; color 线条颜色 (RGB) 或亮度(灰度图像 )
// thickness 组成矩形的线条的粗细程度。取负值时(如 CV_FILLED)函数绘制填充了色彩的矩形。 line_type 线条的类型。见cvLine的描述。 shift 坐标点的小数点位数。
// 两个函数的不同之处在于第二个函数把第一个函数中的两个对角线定点改为了cv::Rect。
void cv::rectangle(cv::Mat& img, cv::Point pt1, cv::Point pt2, const cv::Scalar& color, int thickness=1, int lineType=8, int shift=0)
void cv::rectangle(cv::Mat& img, cv::Rect rec, const cv::Scalar& color, int thickness=1, int lineType=8, int shift=0 )

// 下面通过一个示例,来看看 cv::getTextSize()与cv::putText()相结合的妙用:

{
	//创建空白图用于绘制文字
	cv::Mat image = cv::Mat::zeros(cv::Size(640, 480), CV_8UC3);
	//设置蓝色背景
	image.setTo(cv::Scalar(100, 0, 0));
 
	//设置绘制文本的相关参数
	std::string text = "Hello World!";
	int font_face = cv::FONT_HERSHEY_COMPLEX; 
	double font_scale = 2;
	int thickness = 2;
	int baseline;
	//获取文本框的长宽
	cv::Size text_size = cv::getTextSize(text, font_face, font_scale, thickness, &baseline);
 
	//将文本框居中绘制
	cv::Point origin; 
	origin.x = image.cols / 2 - text_size.width / 2;
	origin.y = image.rows / 2 + text_size.height / 2;
	cv::putText(image, text, origin, font_face, font_scale, cv::Scalar(0, 255, 255), thickness, 8, 0);
 
	//显示绘制结果
	cv::imshow("image", image);
	cv::waitKey(0);
	return 0;
}

=================================================================

4.opencv中 cv::Mat 与 cv::Rect 在一起使用的情况

    // cv::Mat frame; 
    // cv::Rect rect;
    //创建的新图像img是fram的一部分,具体的范围rect指定,此构造函数也不进行图像数据的复制操作,新图像img与frame共用图像数据。
    cv::Mat img(frame, rect);  //cv::Mat::Mat(cv::Mat const&, cv::Rect_<int> const&) 这种方式构造函数创建的img,他的两个参数都不能修改,因为是const,修改后就会出错。

    cv::Mat img1(img2);	 //拷贝构造
    img1(rect);  //从img1中截取rect区域,返回cv::Mat。

=================================================================

5.imshow()显示图像帧时暂停键的用法

	严格按照下面这种方式和顺序不会有问题,例如在imshow后面直接跟cv::waitKey(1)后再char c=...会有问题。
	cv::namedWindow("light_object", cv::WINDOW_NORMAL);
	cv::resizeWindow("light_object", 640, 480);
	cv::imshow("light_object", frame);
	char c = static_cast<char>(cv::waitKey(50));
	if (c == ' ') {
	cv::waitKey(0);
	}

=================================================================

6.opencv播放视频 以及 设置任意键为暂停键

【该方法不实用,可能会暂停不了,时灵时不灵】
对于cv::VideoCapture的使用可以参考 https://blog.csdn.net/guduruyu/article/details/68486063

    cv::Mat img;
    std::string videopath="./path/..";
    cv::VideoCapture cap;

    cap.open(videopath);
    
    if (!cap.isOpened()) {
     std::cout << "Can't open the video file!"<<std::endl;
     return -1;
    }
    
    cap >> img; // 把捕获的帧传送给img。在这一句后面设置暂停键
    //cap.read(img); // 这句话和cap >> img等价。
    // 设置任意键为暂停键,即暂停捕获视频中的帧
    if(cv::waitKey(1)>=0)		//有键盘上的键按下时,该函数返回值>=0;没有键按下时,该函数返回值为-1。cv::waitKey(1)为等待1ms;
        cv::waitKey(0);			//cv::waitKey(0);为一直等待直到有键按下。

    // 判断是否图片是否为空,为空说明没有打开video。
    if (img.empty()) {
        cap.open(videopath);
    }

    // 设置任意键暂停
    if(cv::waitKey(1)>=0)
    cv::waitKey(0);

    // 推荐用下面这种方式
    // cv::waitkey()中的延迟时间需要根据程序定义,太少了捕捉不到按键,我目前没碰到这种情况,碰到设置再大延时也捕获不到按键的情况。
    // cv::waitKey(1)括号中必须填上1或者别的数字,否则会一直等待键盘按下,当键盘某键按下时,会返回该键的ascii码
        char key=cv::waitKey(3);
        if(key ==32)     //空格键暂停,空格键对应的ascii码是32
            cv::waitKey(0);  
        if(key ==9){   //tab键快进30帧,设置不了使用右箭头
            for(int i=0;i<30;i++) //快进30帧
                cap >> img;	//快进多少帧,就把这一句执行多少次
        }

【不推荐】使用下面这种方法,因为我在apollo上使用根本捕获不了按键,我目前还不知道是什么原因,必须使用以上方式把cv::waitKey(1)先赋值给一个char变量才行。

		if(cv::waitKey(3) ==32)     //空格键暂停
			cv::waitKey(0);  
		if(cv::waitKey(3) ==9){   //tab键快进30帧,设置不了使用右箭头
			for(int i=0;i<30;i++) //快进30帧
				cap >> img;		//快进多少帧,就把这一句执行多少次
		}

=================================================================

7.opencv旋转图片

Python版本的图片旋转:

    //重要的是看注释看懂原理,c++版本的是同样原理,函数接口功能一样,只不过函数接口的参数可能不一样。
		
    img=cv2.transpose(img) //对矩阵做转置后,并非旋转了90度。需要再配合翻转才行
    // # 上面一句搭配下面一句话是逆时针旋转90度
    // # img=cv2.flip(img, 0, img)   #第二个参数是flipcode。  flipcode=0,则在X轴上做镜像,如果flipcode=1,则在Y轴上做镜像,如果flipcode=-1则在两个轴同时作镜像
    // # 搭配这句话就是顺时针旋转90度
    img=cv2.flip(img, 1, img)

C++版本的旋转:

    cv::Mat img;   
    // 以下二者结合使用就是:顺时针旋转90度
    cv::transpose(img, img);  //对矩阵做转置后,并非旋转了90度。需要再配合翻转才行
    cv::flip(img, img, 1);	  // 第三个参数是flipcode,flipcode=0,则在X轴上做镜像,如果flipcode=1,则在Y轴上做镜像,如果flipcode=-1则在两个轴同时作镜像.

=================================================================

8.const和constexpr相关知识

记忆法:             
const是常量,*是指针,const *按顺序念就是【常量指针】,* const 按顺序念就是【指针常量】。

const int * p;	//p是常量指针,p是const int *,const限定的是int,即指针指向的地址中存储的值(int型),所以这种写法不能修改指针指向的地址的中存储的值,但是可以修改指针的指向。
int * const p;  //p是指针常量,p是int*,const限定的是指针,即不能修改指针指向,但是可以修改指针指向的地址中存储的值,

类中的this指针指向成员函数所属的类对象,并且this指针只能在类的成员函数中调用。
this指针的本质,就是指针常量, 即【类名 * const this】,this指针的指向不能修改,如 this=NULL 是不合法的;但是this->m_Age=30是合法的,其中m_Age是类成员变量。

const 类名 * const this
常函数:在类的成员函数的括号后面,{}前面,加const就是常函数。
	常函数内不允许修改成员变量,除了mutable定义的成员变量。
	在常函数中不能修改指针指向的值,如this->m_Age=30不合法。可以在变量前加上mutable关键字mutable后就可以修改了。
	如果类成员函数func()定义为常函数:
	 	int func()const{...}
		即在类的成员函数后面加const,相当于【const 类名 * const this】中的第一个const,修饰的是this指针,this指针指向的值也不能修改;
		又由于本来this指针就不能改变指向,所以这种写法就既不能改变this指针的指向,也不能改变this指针调用的成员变量的值,即在该函数中this->m_Age=30和 this = NULL 都不合法。

常对象:初略记了一下,并不完整。
常对象只能调用常成员函数,常对象不允许修改成员变量,但是并不是说常对象所属的类的成员函数都是常成员函数,他也包括非常成员函数。
1.常函数成员既可以使用常数据成员也可以使用非常数据成员;
2.只有常成员函数才可以操作常对象,即常对象只能调用常成员函数。

class Person{
	public:
		int m_Age;
		mutable int m_Height;	//加上mutable关键字后就可以对const限定不能修改值的变量进行赋值了。
};
void test() {
	const Person person1;	//不能修改常对象person1的各个成员变量的值。
	person1.m_Age = 30;  	//不合法
	person1.m_Height = 180; // 合法
}


const有两个作用,1.表示只读;2.表示常量。

C++11中可以使用const来表示只读,用constexpr表示常量表达式。
常量表达式可以在编译阶段就直接计算出来,提高程序运行效率。而非常量表达式只能在程序运行时计算出来。

可以使用constexpr限定函数也为常量表达式,前提是该函数中只能操作常量。假如该函数中使用了任意非常量的操作,例如for(it i=0;i<10;++i)循环语句,constexpr就会被忽略。

=================================================================

9. a.cc文件中调用另一个b.cc文件中的函数

(1)两个.cc文件(或者.h文件)不要互相#include,否则会出错。          
(2)每个.cc(.h)文件一般只定义一个class,一个a.cc(a.h)文件要使用到另一个b.cc(b.h)文件中的函数func时候,首先在a.h中包含b.h头文件#a.h,然后使用class_name在a.h中的类中声明一个对象bb_,然后在a.cc中使用bb_.func()这种用法。
    //a.h
    class a{};
    //b.h
    class b{void func();};
    a.cc中要使用b.h中的func(),需要在class a{}中这么声明:
    class a{b bb_;}  
    然后在需要使用到b.h中的func()中的地方这么使用:
    bb_.func();

=================================================================

10.视频编码格式与封装格式,opencv2.4.8不能读取h265编码格式的视频文件

(1)编码格式是编码格式,封装格式(也可以说容器)是封装格式。编码格式有H.265、JPEG、MPEG-4 Video等,封装格式有MP4、AVI等。
opencv2.4.8不支持读取h.265编码格式,opencv3.4可以读取h.265编码格式的视频。所以还是简单粗暴的这么干吧:
ffmpeg -i source_video.mp4  -vcodec mpeg4 final_video.mp4

(2)转换MP4到avi如果有模糊,那是因为码率设置不当,视频信息有损失。
因此使用MP4Box直接把h265的封装格式转换为avi封装格式,速度相当于拷贝,肯定不存在改变编码方式,此时MP4的编码格式还是H265。
安装MP4Box可以参考https://blog.csdn.net/tianlong_hust/article/details/9273875,安装时间稍微有点长

sudo apt-get install libmp4v2-dev
MP4Box -add 3_20_0_50_26_camera72.h265 -fps 25 -new 3_20_0_50_26_camera72.avi

=================================================================

11.当使用imshow显示每一帧画面时,出现卡顿的解决办法

读取每一帧在哪个{}中,imshow()就应该在哪个{}中,必须出现在同等级的{}中,不能出现在他下面的二级{{imshow()}}中。否则,当二级{}不符合,执行不到二级{}里面的imshow()时候,就会出现画面卡顿。

=================================================================

12.cv::VideoWriter的使用,把图片写进video

//注意:输出的视频名称中必须有数字,否则会报错,CAP_IMAGES: can't find starting number (in the name of file):....,可以查看源码,这个错误不查看源码根本就找不到问题所在。
  cv::VideoWriter video_writer_;
  int frame_count_;
  //初始化	
  if (FLAGS_use_test_mode) {
    // int codec = CV_FOURCC('M', 'J', 'P', 'G');
    int codec = cv::VideoWriter::fourcc('M', 'J', 'P', 'G');
    const std::string output_video_file = FLAGS_camera_name + ".avi";	//视频名称必须包含数字
    video_writer_.open(output_video_file, codec, 10, cv::Size(1920, 1080));
    if (!video_writer_.isOpened()) {
      SERROR << "Can't create output video file: " << output_video_file;
      return;
    }
  }

	//当程序运行到这里时,就开始把图片写进video.
  if (FLAGS_use_test_mode) {
    SRETURN_IF(FLAGS_skip_frame_num <= 0);
    if ((frame_count_ < FLAGS_max_save_frame_count * FLAGS_skip_frame_num) &&
        ((frame_count_ % FLAGS_skip_frame_num) == 0)) {

      cv::Mat rgb_img=frame;
      if ((rgb_img.cols != 1920) || (rgb_img.rows != 1080)) {
        cv::Mat dst_img;
        cv::resize(rgb_img, dst_img, cv::Size(1920, 1080), 0, 0,
                   cv::INTER_NEAREST);
        video_writer_.write(dst_img);
      } else {
        video_writer_.write(rgb_img);	//图片写进视频
      }
    }
    if (frame_count_ > FLAGS_max_save_frame_count * FLAGS_skip_frame_num) {
      video_writer_.release();
    }
    ++frame_count_;
  } // if FLAGS_use_test_mode	

=================================================================

13.C++11多线程join()和detach()的理解

原文链接:https://blog.csdn.net/qq_36784975/java/article/details/87699113
上面这个链接的博客中介绍得很详细.

join()函数,是一个等待线程完成函数,主线程需要等待子线程运行结束了才可以结束.
detach()函数,称为分离线程函数,使用detach()函数会让线程在后台运行,即说明主线程不会等待子线程运行结束才结束.

总结
join()函数是一个等待线程函数,主线程需等待子线程运行结束后才可以结束(注意不是才可以运行,运行是并行的),如果打算等待对应线程,则需要细心挑选调用join()的位置
detach()函数是子线程的分离函数,当调用该函数后,线程就被分离到后台运行,主线程不需要等待该线程结束才结束.

=================================================================

14.获取鼠标动作进行相应处理

int main(){
    cv::imshow("EnvFusion", img); //这里显示一个窗口
    /*<!-- 在窗口上进行鼠标操作就使用  cv::setMouseCallback()-->
    <!-- 可进行的操作有 滑轮滚动,左键按下,右键按下,鼠标移动,
    还可以组合,比如鼠标左键按下并且鼠标移动,就是鼠标在窗口中拖动 -->
    <!-- 该函数也会获取鼠标点击的x,y -->*/
    cv::setMouseCallback("EnvFusion", OnMouseAction);
}

// <!-- 鼠标操作回调函数 -->
  void OnMouseAction(int event, int x, int y, int flags, void *ustc) {
    double value;
    float step = 0.02;
    switch (event) {
      case CV_EVENT_MOUSEWHEEL:
        value = cv::getMouseWheelDelta(flags);
        if (value > 0)
          map_scale = map_scale * 0.9;
        else if (value < 0)
          map_scale = map_scale * 1.1;
        if (map_scale > 20) map_scale = 20;
        if (map_scale < 0.05) map_scale = 0.05;
        break;
      case CV_EVENT_LBUTTONDOWN:
        MousePress(x, y);
        map_down = true;
        prept = cv::Point(x, y);
        break;
      case CV_EVENT_LBUTTONUP:
        map_down = false;
        break;
      default:
        break;
    }

  if (map_down == true && event == CV_EVENT_MOUSEMOVE)  //左键按下且鼠标移动
  {
    cv::Point curpt = cv::Point(x, y);
    cv::Point dpoint0 = curpt - prept;
    dpoint += dpoint0;
    prept = curpt;
  } else {
    dpoint = cv::Point(0, 0);
  }
 }
void MousePress(int x, int y) {
  <!-- 这里的x,y是传进来的鼠标点击位置的坐标 -->
  <!-- 一般是用来判断鼠标点击的坐标(x,y)是不是在某个范围内,是的话就进行某种操作 -->
}

=================================================================

15.查找与删除map和vector容器中指定元素

(1) map

查找map的关键字:

std::map<std::string,int> map_name_;

auto iter = map_name_.find("key_name");
if (iter != map_name_.end()) {
   //找到了该关键字,进行的操作
}
删除map的指定key值

有两种方法:
方法1:直接删除key
map_name_.erase("key_name");

方法2:使用迭代器删除

auto iter = map_name_.find("key_name");
if (iter != map_name_.end()) {
  map_name_.erase(iter);  //删除
}

(2) vector

删除vector的指定元素"123"

方法1:使用迭代器
不同于map(map有find方法),vector本身没有find这一方法.

std::vector<std::string> vct_name_;
auto iter = vct_name_.begin();
while(iter != vct_name_.end()) {
  if(*iter=="123"){ 		 // 这命令可以作为查找vetor元素的方法
    vct_name_.erase(iter);	 // 删除
  }
}

方法2:使用 std::remove_if

std::vector<std::string> vct_name_;
vct_name_.erase(std::remove_if(vct_name_.begin(),vct_name_.end(),[](std::string str) { return str == "123"; }),vct_name_.end());

15.1 map 的insert和emplace方法

参考地址: https://www.cnblogs.com/khacker/p/10479801.html
对于std::mapstd::unordered_mapinsert(std::make_pair(key, value))emplace(std::make_pair(key, value))重复插入同一个key的操作,二者都不会替换原先的key对应的value值,只有索引[]操作会改变value。

    std::unordered_map<int, int > map;
    map.insert(std::make_pair(1, 1));
    map.insert(std::make_pair(2, 2));
    map.insert(std::make_pair(3, 3));
    map.insert(std::make_pair(1, 4)); //这一步并不会改变key为1的value值,仍旧是1,不会变为4
	std::unordered_map<int, int > map;
    map.emplace(1, 1);
    map.emplace(2, 2);
    map.emplace(3, 3);
    map.emplace(1, 4);  //这一步并不会改变key为1的value值,仍旧是1,不会变为4
    map[1] = 1;
    map[2] = 2;
    map[3] = 3;
    map[1] = 4; //这句话会改变key为1的value值,变为4

15.2 map的erase(iter)需注意,和vector不一样

https://blog.csdn.net/zhangyueweia/article/details/50293965

#include <iostream>
#include <map>
#include <string>
#include <vector>

// g++ -std=c++11 main.cpp -o main

int main() {
  // std::vector可以直接删除iter后不影响遍历
  std::vector<std::string> name_vct = {"Alibaba", "Baidu", "CMD", "DDS",
                                       "Ella"};
  std::vector<std::string>::iterator it = name_vct.begin();
  while (it != name_vct.end()) {
    std::cout << "*it= " << *it << std::endl;
    if (*it == "CMD") {
      name_vct.erase(it);
      std::cout << "erase后 *it= " << *it << std::endl;
    } else {
      ++it;
    }
  }

  std::cout << std::endl;

  std::map<std::string, int> name_age_map = {
      {"AAA", 21}, {"Bob", 22}, {"Cool", 23}, {"Daisy", 24}};

  /**  下面这种方式会出错。 std::map 删除iter后继续遍历会造成double free **/
  /*
    auto it0 = name_age_map.begin();
    while (it0 != name_age_map.end()) {
      std::cout << "key= " << it0->first << std::endl;
      if (it0->first == "Bob") {
        name_age_map.erase(
            it0);
    //当这条语句执行完后,it1就是一个非法指针,如果再执行++就会出错. std::cout
    << "erase后 it->first= " << it0->first << std::endl; } else {
        ++it0;
      }
    }
  */

  /** --方法1
   * std::map删除iter需要这么使用,使用一个临时变量保存迭代器后,将迭代器自增1
   **/
  /*
   while  (it1 != name_age_map.end()) {
     std::cout << "key= " << it1->first << std::endl;
     if (it1->first == "Bob") {
       auto iter_tmp = it1;
       ++it1;
       name_age_map.erase(iter_tmp);
       std::cout << "erase后 iter_tmp->first= " << iter_tmp->first <<
       std::endl; std::cout << "erase后 it->first= " << it1->first <<
       std::endl;
     } else {
       ++it1;
     }
   }
   */

  /** --方法2
   * std::map删除iter需要这么使用,it2 = name_age_map.erase(it2);
   **/
  /*
 auto it2 = name_age_map.begin();
 while (it2 != name_age_map.end()) {
   std::cout << "key= " << it2->first << std::endl;
   if (it2->first == "Bob") {
     it2 = name_age_map.erase(it2);
     std::cout << "erase后 it->first= " << it2->first << std::endl;
   } else {
     ++it;
   }
 }
*/

  /** --方法2
   * std::map删除iter需要这么使用,name_age_map.erase(it3++);
   **/

  auto it3 = name_age_map.begin();
  while (it3 != name_age_map.end()) {
    std::cout << "key= " << it3->first << std::endl;
    if (it3->first == "Bob") {
      name_age_map.erase(it3++);
      std::cout << "erase后 it->first= " << it3->first << std::endl;
    } else {
      ++it3;
    }
  }
}


=================================================================

16.sprintf的用法

double db=10.123456;
char aaa[20];
std::sprintf(aaa,"qqq:%.1f",db);   //aaa[]就变成了qqq:10.1,保留一位小数
cv::putText(img, aaa, cv::Point(100 , 100),
    cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(255, 255, 0), 2);  //在img上显示文字内容
std::string str(aaa); //str就变成了qqq:10

通常情况下,需要批量命名文件时候,会用到

char pic[50];
static int count = 0;
if (count % 10 == 0) {
  //下面的track_id是一个整型变量。picture/目录需要自己新建。不想自己手动创建可以百度查询如何使用linux下c++创建文件夹。
  sprintf(pic, "picture/id_%d_%d.png", track_id, count);	
  cv::imwrite(pic, rgb_img);  //这句话会把命名的文件存在picture目录下。
}
++count;

=================================================================

std::shared_ptr、std::make_shared、 .get() 、.data()、void *p 的用法、裸指针

(1)shared_ptr能够记录对象被引用的次数,主要被用来管理动态创建的对象的销毁。
shared_ptr是一种智能指针(smart pointer)。shared_ptr的作用有如同指针,但会记录有多少个shared_ptrs共同指向一个对象。
这便是所谓的引用计数(reference counting)。一旦最后一个这样的指针被销毁,也就是一旦某个对象的引用计数变为0,这个对象会被自动删除。这在非环形数据结构中防止资源泄露很有帮助。
(2)如果事先知道所需内存空间,使用静态内存是最简单的解决方案。

但是,在程序设计的过程中,往往会遇到需要开辟一个未知大小的内存空间,该空间根据程序所需发生大小的变化,此空间称为动态内存。

程序设计中使用动态内存的原因可能如下:

(1)程序不知道自己需要多少对象;

(2)程序不知道所需对象的准确类型;

(3)程序需要在多个对象之间共享数据

shared_ptr的用法:可以指向特定类型的对象,用于自动释放所指的对象。

make_shared的用法:make_shared在动态内存中分配一个对象并初始化它, 返回指向此对象的shared_ptr,与智能指针一样,make_shared定义在头文件memory中;

当要用make_shared时,必须指定想要创建的对象类型,定义方式与模板类相同,在函数名之后跟一个尖括号,在其中给出类型;

make_shared<int>p3 = make_shared<int>(42)

一般采用auto定义一个对象来保存make_shared的结果,如auto p1 = make_shared<int>(42);

(3) std::shared_ptr::get() 返回存储的指针,指向 shared_ptr 对象解引用的对象,通常与其拥有的指针相同。
(4)C++ vector.data()返回指向vector中第一个数据的指针,或空vector之后的位置
(5) void的字面意思是“无类型”,void *则为“无类型指针”,void *可以指向任何类型的数据。
void指针指向的数据类型未定,将其值赋给其他值时要类型转换,但是任何类型的指针都可以直接赋值给void*,无需进行强制类型转换
参考https://blog.csdn.net/qq_33890670/article/details/79964262
(6) 裸指针,一般指的就是类似 int *p;这种方式!!
一般情况下,可以用智能指针替换裸指针。
参考:https://blog.csdn.net/qq_38684512/article/details/103421567

=================================================================

18.选择使用memcpy_s,strcpy_s还是选择strcpy,memcpy? memset()的用法

memcpy_sstrcpy_s函数明确的指定了目标内存的大小,能够清晰的暴露出内存溢出的问题,而普通的strcpymemcpy则不会。

为了保证内存拷贝有足够的空间,防止笔误,【尽量使用memcpy_s代替memcpy】。
(1)memcpy_s()的用法:

errno_t memcpy_s(
   void *dest,
   size_t numberOfElements,
   const void *src,
   size_t count 
);  // 注意,numberOfElements >= count,否则会出现异常

memcpy_s 复制srccount 字节到 dest;如果destsrcnull指针,或者 numberOfElements 为缓冲区太小,会抛出异常。

void Raw2Mat(CameraPublishDecodedStruct src_yuv_img,cv::Mat &frame) {
  std::shared_ptr<hiai::ImageData<u_int8_t>> input_image =
      std::make_shared<hiai::ImageData<u_int8_t>>();
  input_image->size = src_yuv_img.raw_data.size();
  // input_image->data = std::shared_ptr<u_int8_t>(new 
  // 		u_int8_t[src_yuv_img.raw_data.size()]); 
  // memcpy_s(input_image->data.get(), src_yuv_img.raw_data.size(),
  //		  src_yuv_img.raw_data.data(), src_yuv_img.raw_data.size());
  //上面注释的两句话可以用下面这句代替
  input_image->data = std::shared_ptr<u_int8_t>(
      const_cast<u_int8_t *>(src_yuv_img.raw_data.data()), [](u_int8_t *p) {});
  input_image->height = src_yuv_img.height;
  input_image->width = src_yuv_img.width;

  cv::Mat rgb_img(input_image->height, input_image->width, CV_8UC3);
  cv::Mat yuyv_img =
      cv::Mat(input_image->height * 3 / 2, input_image->width, CV_8UC1,
              static_cast<void *>(input_image->data.get()));
  cv::cvtColor(yuyv_img, rgb_img, cv::COLOR_YUV420sp2RGB);
  frame = rgb_img;
}

(2) memset()函数的用法
memset函数详细说明
1)void *memset(void *s,int c,size_t n)
总的作用:将已开辟内存空间 s 的首 n 个字节的值设为值 c。
2) memset()函数常用于内存空间初始化。如:

char str[100];
memset(str,0,100);

3)memset可以方便的清空一个结构类型的变量或数组。 如:

	struct sample_struct{
	           char csName[16];
	            int iSeq;
	            int iType;
	};
	
	
	
	对于变量:
	struct sample_strcut stTest;
	
	一般情况下,清空stTest的方法:
	stTest.csName[0]='/0';
	stTest.iSeq=0;
	stTest.iType=0;
	
	用memset就非常方便:
	memset(&stTest,0,sizeof(struct sample_struct));
	
	
	
	如果是数组:
	struct sample_struct TEST[10];
	则
	memset(TEST,0,sizeof(struct sample_struct)*10);
	
	#include <mem.h>
	void* memset(void* s, int c, size_t n){
	          unsigned char* p = (unsigned char*) s;
	
	          while (n > 0) {
	                     *p++ = (unsigned char) c;
	                       --n;
	           }
	
	          return s;
	}

memset()的函数, 它可以一字节一字节地把整个数组设置为一个指定的值。memset()函数在mem.h头文件中声明,它把数组的起始地址作为其第一个参数,第二个参数是设置数组每个字节的值,第三个参数是数组的长度(字节数,不是元素个数)。其函数原型为:
void *memset(void*,int,unsigned);
  其中void*表示地址。
  例如,下面的代码用数组做参数传递给标准函数memset(),以让其将数组设置成全0:

#include<mem.h>
void main()
{
	int ia1[50];
	int ia2[500];
	memset(iai,0,50*sizeof(int));
	memset(ia2,0,500*sizeof(int));
}

memset()的第一个实参是数组名,数组名作参数即数组作参数,它仅仅只是一个数组的起始地址而已。
  在函数memset()栈区,从返回地址往上依次为第1,2,3个参数。第1个参数中的内容是main()函数中定义的数组ia1的起始地址。第2个参数是给数组设置的值(0),第3个参数是数组的长度(50*2)。函数返回时,main()函数的数组中内容全置为0。

19.C++强制类型转换(dynamic_cast,static_cast, const_cast, reinterpret_cast)

参考 https://blog.csdn.net/muyuyuzhong/article/details/82699374

20.编译proto的问题

重装protoc可参考 https://blog.csdn.net/u013498583/article/details/74231058

查看当前protoc版本: protoc --version

查看protoc安装位置:which protoc

查找protoc相关文件:sudo find / -name protoc

编译proto文件
protoc caffe.proto --cpp_out=./ 生成caffe.pb.h、caffe.pb.cc文件
protoc caffe.proto --python_out=./ 生成caffe_pb2.py文件
假如环境中装了一个以上的protoc版本,可使用protoc双击tab键,会弹出所有版本(protoc protoc_2.6.1,其中protoc是默认版本,protoc --version是查看当前默认protoc版本)。
如果我们要使用非默认版本protoc编译proto,可以protoc_2.6.1 caffe.proto --cpp_out=./这种方式编译。
在编译MobileNet-YOLO工程时,使用protoc protoc_2.6.1编译后的 caffe.pb.hcaffe.pb.cc 文件粘贴到 caffe\include\caffe\proto即可。

21.无符号整型变量比较大小时用减法越界的bug问题

无符号(unsigned)和有符号(signed)两种类型(floatdouble 总是带符号的),在除char以外的数据类型中,默认情况下声明的整型变量都是有符号的类型;char在默认情况下总是无符号的。在除char以外的数据类型中,如果需声明无符号类型的话就需要在类型前加上unsigned。

当两个无符号整型变量如std::uint64_t x1,x2;比较大小时,应使用if(x1>x2)这种方式,别用if(x1-x2>0),因为对于无符号整形变量,二者相减是不会为负数的.
只有符号变量直接相减来比较二者的差值。
如果使用了无符号整型变量,可先限定条件if(x1>x2),然后再使用减法delta=x1-x2.

std::uint64_t delta;
if(x1>x2){
  delta=x1-x2;
}else{
  delta=x2-x1;
}
// delta = x1>x2 ? (x1-x2):(x2-x1);  //直接一句话可以代替上面的if-else
std::max(A,B)比较二者谁更大,返回更大的值.
std::min(A,B)比较二者谁更小,返回更小的值.

22.摄像头焦距和视场角

摄像头焦距和视场角:
广角镜头: 视场角FOV越大,焦距越小,则视野越大,物体越小,看的越近,远处的物体看不清;
长焦镜头: 视场角FOV越小,焦距越大,则视野越小,物体越大,看的越远,近处的物体看不到;

比亚迪和106等mdc设备车使用视场角命名的摄像头,如28度,60度,100度,182度. 28度看的最远.
福田1和福田2使用焦距命名的摄像头:如6mm,12mm,25mm. 25mm摄像头看的最远.

23.RGB和BGR的转化,通道分离与合并

opencv中默认读取的图片格式是BGR,并非RGB.
下面是opencv的更直接BGR转RGB方法:
cv::cvtColor(bgr_img, rgb_img, cv::COLOR_BGR2RGB);

下面是通道的分离与合并:

    std::vector<cv::Mat> MatVct_1(3);
    cv::split(src_img, MatVct_1); //这句话把src_img分离为三个Mat
    
    std::vector<cv::Mat> MatVct_2; 
    
    MatVct_2.push_back(MatVct_1[0]); //如果这里[0]改为[2],下面的[2]改为[1],就是bgr转rgb了
    MatVct_2.push_back(MatVct_1[1]);
    MatVct_2.push_back(MatVct_1[2]);

    cv::merge(MatVct_2, dst_img); //这句话把MatVct_2合并为一个3通道的Mat

24.重写、覆盖、using、typedef

父类如果定义某非虚函数func1(int,int),子类定义了函数func1(double),那么子类不能再调用func1(2,3),因为子类只要定义了父类同名函数,不管他们参数类型和个数是否不同,都会隐藏父类的同名函数,相当于覆盖了父类的所有同名函数。

如果自己又要自己定义还同名函数,又想使用父类的这同名函数,c++11可以通过在子类中使用using 父类名::func1;即可。

注意,重写和覆盖不是一个意思。重写是虚函数在子类中重新定义,使用override,override也可以不写,加上override是规范,语义更清晰明白它是重写虚函数。
覆盖是子类定义同名函数覆盖掉父类同名函数。

同理,如果父类有几个复杂的构造函数,子类想继承父类所有构造函数,c++11可以在子类中使用 using 父类名::父类名;如:

class Base{
 public:
    Base();
    Base(const Base &){
       // 很复杂的一些初始化语句
    };
    func1(int,int);
};

class Child : Base{
  public:
    using Base::Base;     //加上这句话,子类就继承了父类的所有构造函数
    using Base::func1();  // 加上这句话,就可以使用基类被覆盖(隐藏)的所有同名函数func1了。
    func1();              //该定义会直接覆盖掉基类所有同名函数func1.  解决办法就是使用上面的using Base::func1();
};

using 的用法:

using namespace std;
using namespaceA::namespaceB::func; // 调用命名空间B下的func()函数,该声明语句不要写成func(),不能加括号。

using anotherName = int; //取别名。一般用在某类型特别长的时候,取个短点的别名。

注意区分typedef,typedef和using在取别名上作用基本一样,只是顺序不同,如:typedef int anotherName;
struct 和 class的作用基本一样,但是常规用法是,struct 访问类型默认是public,并且一般用在把一些变量封装成一个结构体变量。class就是通常的用法。

25.静态成员静态成员函数/变量、单例模式

静态成员变量和静态成员函数 是属于类,不是属于对象。
非静态成员变量和非静态成员函数,是属于对象的,如果没有实例化对象,那么他们是不存在的。
所以调用静态成员函数时,既可以使用 类名::静态成员函数 的方式来调用,也可以使用对象.静态成员函数函数 的方式来调用。

this是指向对象的,没有把类实例化成对象的话,是没有this指针的,所以,在静态成员函数的定义中,不能使用this指针。

非静态成员变量属于对象,不属于类,没有实例化对象,就不存在非静态成员变量。静态成员变量是在编译阶段生成,而非静态成员变量是在运行时生成。

静态成员函数属于类,没有this指针,所以它不能对非静态成员变量直接进行操作,但是静态成员函数可以调用非静态成员函数(非静态成员函数虽然可以操作静态成员变量,但是应该禁止),
例如单例模式的公共接口静态成员函数Instance()就可以调用所有成员函数。

即,静态成员函数可以被类直接调用,也可以被对象来调用。非静态成员函数既可以被静态成员函数调用,也可以被非静态成员函数调用。

对于单例模式,是把类A的构造函数私有化,然后私有化一个静态对象static A a;并提供一个公有的静态函数static A *Instance(){return a;},该函数返回值为静态A对象a。

class A{
 public:
    static A &Instance(){      //引用类型,在外部调用其他public函数方法 A::Instance().func();
     return a;
    }
    //static A *Instance(){    //指针类型,在外部调用其他public函数方法 A::Instance()->func();
    //	return a;
    //}

   void func(){
    std::cout<<"调用func()";
   }

 private:
    A();
    A(const A &);
    A& operator=(const A&);
    static A a;
};

这样在外部就只能使用A::Instance()来调用成员函数,并且只能这样调用,返回的是类A的静态对象a,这样就能保证只有一个实例。
如果要调用类A的非静态函数func(),可以使用A::Instance()->func()

26.proto相关用法

例如,有 proto_name.proto文件,把这个 proto_name.proto文件编译后,会产生 proto_name.pb.hproto_name.pb.cc, 生成的.h文件中的class都继承自::google::protobuf::Message类,Message类提供了一些方法可以检查或者操作整个message,包括:

 bool IsInitialized() const;		// 检查是否所有required变量都已经初始化;

 string DebugString() const;		// 返回message的可阅读的表示,主要用于调试程序;

 void CopyFrom(const Person& from);	// 使用一个message的值覆盖本message;

 void Clear();				// 清空message的所有成员变量值。

每个message类都提供了写入和读取message数据的方法,包括

 bool SerializeToString(string* output) const;		// 把message编码进output。 

 bool ParseFromString(const string& data);		// 从string解码到message

 bool SerializeToArray(char* buf,int size) const;	// 把message编码进数组buf.

 bool ParseFromArray(const char* buf,int size);		// 把 buf解码到message。此解码方法效率较ParseFromString高很多,所以一般用这种方法解码。

 bool SerializeToOstream(ostream* output) const;	// 把message编码进ostream

 bool ParseFromIstream(istream* input);			// 从istream解码到message

备注:发送接收端所使用的加码解码方法不一定非得配对,即发送端用SerializeToString 接收端不一定非得用ParseFromString ,可以使用其他解码方法。
用法1:对外接口的消息结构的兼容性

当某个对外接口的消息结构 msgA 中的某个变量的结构总是要根据需求而频繁增删修改的时候,一般会在这个消息结构 msgA 中定义一个 std::string proto_data 的变量,
这样只需要修改 proto_name.proto文件中的内容,而不需要总是修改 msgA(尤其是当修改msgA可能需要修改框架协议的时候,总之就是代价比较大),使用proto方法来解决这个问题就特别方便。

原始结构:

struct msgA{
    MsgHeader header;
    bool is_ok;
    structBBB msgbbb;  //当这个结构体 structBBB 增删修改结构比较麻烦时,会造成频繁修改对外接口 msgA 的麻烦。
}
方法:

先定义一个 proto_name.proto文件,把这个 proto_name.proto文件编译后,会产生 proto_name.pb.h 和 proto_name.pb.cc,
对其中定义的变量赋值是使用 set_xxx(111) 函数进行赋值,赋值完成后再序列化之后进行打包发送;
发送之前使用 proto_name.SerializeToString(&data) 把该 结构序列化为string类型,然后再发送。
原始结构 修改为:

struct msgA{
    MsgHeader header;
    bool is_ok;

   //定义一个string类型的结构来代替 structBBB msgbbb; 然后把proto定义的数据结构 proto_name.proto赋值后,
   //调用 proto_name.SerializeToString(proto_data) 进行序列化转换为string赋值给proto_data变量,这就是使用proto方式来解决。
    std::string proto_data;   
}

下面的函数,我们封装了proto自带的SerializeToString和ParseFromString方法

// msg转换成string
template <class T>
void MsgToString(const T msg, std::string &data) {
    msg.SerializeToString(&data);
}


// 将string转换成msg
template <class T>
void StringToMsg(const std::string data, T &msg) {
    msg.ParseFromString(data);
}
用法2:

当我们需要使用配置文件来给许多变量进行赋值的时候,就可以把这些变量全都定义在 config.proto中,然后建立一个config.pb.txt文件,在config.pb.txt文件中对config.proto中的变量进行设置。
后续进行一些读取config.pb.txt文件中的内容,然后需要的地方获取这些内容。
代码涉及到文件读取解析proto等等的内容,有点复杂,暂时不做展开,这里只是指出有这个用法。

27.深拷贝、浅拷贝、拷贝构造函数 之间的关系

关于拷贝构造、深拷贝、浅拷贝参考https://blog.csdn.net/qq_29344757/article/details/76037255
浅拷贝只拷贝指针,不新开辟内存。深拷贝会另外开辟一块内存,内容和拷贝的对象一样。

所谓拷贝构造,传入的参数限定于是同一类之前创建的对象,用它来初始化新建的对象。

拷贝构造主要就是把别的对象的成员变量的值赋值给自己的成员变量。或者说,直接新开辟一段内存,然后把传入的对象的成员变量的值赋值给自己。并不能直接把其他对象直接复制给自己,没有这种用法。

对于传入的参数是例如int类型之类的构造函数,他不叫拷贝构造函数,它只是简单的构造函数。

默认的拷贝构造函数,对于指针变量是浅拷贝,对于非指针变量是深拷贝。这样在析构时会造成double free的错误。所以必须自己定义拷贝构造函数,在里面对指针变量新分配内存后,再把别的对象的指针里面的值赋给它。
默认拷贝赋值函数也是浅拷贝。
所以类的成员变量中有指针变量时,必须对拷贝构造函数和拷贝赋值函数重新定义。

拷贝构造的作用是防止浅拷贝。
因为如果我们不使用深拷贝而使用浅拷贝的话,对象a浅拷贝对象b,当浅拷贝的对象b析构后,b所指向的内存已经释放,那么a所指向的内存也被释放了,当a自己再析构的时候就析构不了了。
还有就是浅拷贝时,任何一个对象对该值进行修改都会影响另一个对象中的值。

默认拷贝构造函数定义举例:

class A{
public:
//默认拷贝构造函数为
 A(const A& a){
     tmp1=a.tmp1;//深拷贝,不同对象tmp1的地址不一样
     ptr=a.ptr;//浅拷贝,因为ptr为指针变量
 }

private:
    int tmp1;
    int *ptr;
};

我们自己需要重新定义的拷贝构造函数:

A(const A& a){
    tmp1=a.tmp1;//深拷贝,不同对象tmp1的地址不一样
    ptr= new int;
    *ptr=*(a.ptr);//深拷贝,因为他们的ptr的地址不一样了。
}
移动构造函数

移动构造函数就是右值引用构造函数。他是为了实现浅拷贝,复用其他对象中的资源(堆内存),延长其他临时对象的生命周期。

A(A&& a):ptr(a.ptr){
    a.ptr=nullptr;//调用移动构造函数,会先把a对象的指针变量ptr先赋值给自己的指针变量ptr,然后把a.ptr指向空指针,这样a在析构的时候就不会把a.ptr本来指向的内容给释放了。这样自己的ptr指针还是指向那块内存。注意,指针指向的那块内存的值是通过*p=?的方式来修改的,所以修改指针指向并不是修改指针指向的内存的值,不要混淆。
}

另外,关于拷贝构造函数和移动构造函数,他们传入的形参都一样,怎么知道调用哪个呢?程序会判断这个形参是不是临时对象,如果是临时对象,就会调用移动构造函数。

注意上面说的传入的形参,也可以是通过类似A a=func()这种使用方式。其中

A func(){
    A a;
    return a;  //返回一个临时对象。
}

所谓拷贝构造,传入的参数限定于是同一类之前创建的对象,用它来初始化新建的对象。

如果类中有指针类型的成员变量,那就必须定义拷贝构造函数,否则你让别的对象的指针成员变量给他赋值,就是浅拷贝,有内存风险。

`void test(int a, int b){ }`

test是函数的首地址,他是一个函数,类型是void()。
&test表示一个指向函数test这个对象的地址,他是一个指针类型是void(*)()。
所以test和&test所代表的地址值是一样的,但是类型不一样。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值