C++高级编程笔记
继承和多态性
容器STL
多线程和线程锁
函数指针和回调函数:
1. 何为函数指针?
函数指针,即指向函数的指针变量。作为一个指针变量,它同普通的指针变量没有什么区别,都满足指针变量使用的三个基本操作。可以使用函数指针直接调用函数,将函数指针作为另一函数的形参(回调函数),亦可将其作为函数的返回值使用。
2.指针变量的定义
需要注意的是,必须要给指针变量名加上括号,否则其表示的是一个指针类型的返回值;同时函数参数列表不写形参名!
typedef 函数返回值类型 (*指针变量名) (函数参数列表);
// 函数指针变量为Fun1,其函数有一个int类型的形参,且返回值也是int类型
typedef int (*Fun1)(int);
// 函数指针变量为Fun2,其函数无形参,且无返回值
typedef void (*Fun2)(void);
// 函数指针变量为Fun1,其函数无形参,且返回值也是自动类型
typedef void* (*Fun3)(void*);
3.何为回调函数?
回调函数就是一个通过函数指针调用的函数。把被调用函数的指针作为参数,传递给另一个函数的形参,那么这个被调用的函数就是回调函数。回调函数往往不是被直接调用,通常回调函数的函数名会被传递给一个回调注册函数,通过回调函数的注册,就能得到指向这个函数的函数指针,再利用该函数指针去调用相应的回调函数
回调函数在开发中的使用场景,通过是用于SDK的使用,它将调用者和被调用者分开。以我们线阵相机的使用为例,线阵的取图是使用了海康SDK中的回调取图方式,即通过注册一个自定义的回调函数,相机在采集取图后会自动调用该回调函数,往回调函数的形参中传递图像的相应参数。这样一来,我只关心回调函数的操作即可,我无需考虑相机采集转码的具体操作;利用回调函数就将相机复杂的底层操作,同我对图像进行的后处理操作分隔开来。
4.C++如何写一个回调函数?
回调函数是基于C编程的Windows SDK的技术,不是针对C++的,程序员可以将一个C函数直接作为回调函数,但是在C++中不能像定义普通成员函数那样去定义一个回调函数;否则将会造成编译错误。这是因为C++的每个成员函数都有一个this指针,C++通过传递一个指向自身的指针给其成员函数,从而实现成员函数可以访问类的成员变量,这也是为什么不同的类对象可以使用同一个成员函数,但是又不会出现歧义的情况。
如果想在C++中定义一个回调函数,有两种方法可供选择。第一,不将回调函数定义为类的成员函数,而是在类外进行定义,使用C的方式。第二,如果要将回调函数定义为类的成员函数,那么必须将该回调函数定义为静态类型。但是静态成员函数有一个缺点,那就是它不能访问类的成员变量,因为静态成员函数不属于对象,因此它也不能访问某一个对象的成员变量;如果回调函数一定要访问类的成员变量,可以将特定的成员变量也修饰成static类型,或是给回调函数传递一个void*类型的形参,将对象的地址传递到回调函数中,再使用强制类型转换将该自动指针转换成类对象指针,再通过类对象指针去调用类的成员变量即可!
/* 回调函数示例程序 */
/* Practice.h */
#pragma once
#include<iostream>
// 定义用户信息结构体
struct s_user
{
std::string name;
int age;
std::string request;
};
/* 重点 */
// 定义回调函数的函数指针类型
typedef void (*CallbackPtr)(std::string,void* p);
class Practice
{
public:
Practice(s_user* user);
void SetUserInfo(struct s_user* user);
struct s_user* GetUserInfo();
void ShowuserPtr();
// 注册回调函数的函数,形参类型为定义的函数指针类型
void RegisterCallbackFunc(CallbackPtr _callbackPtr);
// 执行回调函数的函数
void InvokeCallbackFunc(std::string re,void* p);
// 回调函数
static void SetUserRequest(std::string re,void* p);
s_user* userPtr;
private:
CallbackPtr callbackPtr;
};
/* Practice.cpp */
#include "Practice.h"
Practice::Practice(struct s_user* user)
{
// 将结构体指针变量传递给类的结构体指针变量
userPtr = new s_user;
userPtr->age = user->age;
userPtr->name = user->name;
userPtr->request = user->request;
}
void Practice::SetUserInfo(struct s_user* user)
{
userPtr->age = user->age;
userPtr->name = user->name;
userPtr->request = user->request;
}
void Practice::ShowuserPtr()
{
std::cout << userPtr->name << std::endl;
std::cout << userPtr->age << std::endl;
std::cout << userPtr->request << std::endl;
}
s_user* Practice::GetUserInfo()
{
return userPtr;
}
void Practice::RegisterCallbackFunc(CallbackPtr _callbackPtr)
{
callbackPtr = _callbackPtr;
}
// 回调函数的执行一般由SDK实现,它对用户是透明的
// 用户关心是只是回调函数的注册和回调函数的内容
void Practice::InvokeCallbackFunc(std::string re,void* p)
{
callbackPtr(re,p);
}
static void SetUserRequest(std::string re,void* p)
{
// 将自动指针p强制转换为类对象指针_p
Practice* _p = (Practice*)p;
_p->userPtr->request = re;
}
// userPtr有三个属性值,name和age在对象实例化的时候就指定了
// age则通过回调函数对其进行修改操作
// 程序在执行的时候会对SetUserRequest()进行注册
// 随后通过调用InvokeCallbackFunc()来执行回调函数
// 调用的时候传递了两个参数,即将用户的需求传递给回调函数
int main()
{
struct s_user s1;
struct s_user* s2;
s1.name = "张三";
s1.age = 18;
s1.request = "无";
Practice pr = Practice(&s1);
// 将回调函数SetUserRequest()进行注册
pr.RegisterCallbackFunc(SetUserRequest);
pr.InvokeCallbackFunc("手套", &pr);
pr.ShowuserPtr();
}
4.1回调函数的小结
- 回调函数只能是全局的或是静态的
- 全局函数会破坏类的封装性,故不予采用
- 如果将回调函数定义为static类型,要么将特定的成员变量转成static类型,要么给回调函数传递一个void*类型的形参
- 回调函数的使用分成三部分——回调函数 回调函数的注册 回调函数的调用,一般回调函数是通过SDK形式进行调用的,用户只需关心回调函数的注册和回调函数的内容,回调函数的调用一般是SDK实现的,它对用户是透明的
5.本小节学习的意义?
回调函数的定义和使用是C++高级编程的重要思想,在后面学习怎么写C++的静态库和动态库至关重要,掌握写库的基本操作,有利于将程序的细节隐藏起来,同时将用户和开发者分开,对我们项目本身的安全性具有一定意义,后面可以试着自行写动态库,将程序的关键细节封装起来。
引用:
1.何为引用?
引用本身并不是一个新的变量,而且变量的别名。我们可以将引用理解为一种隐式指针,即它无需解引用的方式操作变量,在很大程度上避免了指针操作起来麻烦,同时也提高的代码可读性。可以让你用一种隐式指针的方式使用指针,即你看起来是在使用普通的变量,但它其实是指针变量。它的作用在作为函数形参进行传递中,有很大的作用,这种情况下可以避免形参在函数调用结束后就会回收了,但同时它又不像指针那么难操作。
2.引用的细节
- 引用的数据类型要与原变量名的数据类型相同
- 引用名和原变量名可以互换,它们值和内存单元是相同的
- 必须在声明引用的时候初始化,如果不对引用进行初始化则会造成编译不通过
- 一个变量可以拥有多个引用,但是一个引用只能针对一个变量
- 不能对一个常量使用引用,但是指针可以被一个常量赋值
3.用于函数形参的引用
函数形参往往具有一定的局限性,即形参在函数执行完毕后将会被回收,因此如果想给函数的形参在函数执行完毕后不被回收,可以将形参指定为指针类型;但是指针类型在使用的时候会比较麻烦,同时也十分容易和乘法运算符相混淆,因此可以使用引用的方式,隐式地使用指针类型形参。但是必须考虑一点,引用形式的形参不能直接传递常量,因此如果要给引用形式的形参传递常量,则必须要将形参定义成const类型,这情况下C++将创建正确类型的匿名变量,将实参的值传递给匿名变量,并让形参来引用该变量。同时,将引用形参声明为const的理由有三个:
- 使用const可以避免无意中修改数据的编程错误
- 使用const使函数能够处理const和非const实参,否则将只能接受非const实参
- 使用const,函数能正确生成并使用临时变量
构造函数和匿名对象:
1.构造函数的种类?
- 默认构造函数 —— 一般都是无参构造函数
- 有参构造函数
- 拷贝构造函数
这里必须要说明一点,即使在定义类的时候没有定义构造函数或是析构函数,C++也会默认给类指定一个构造函数或是析构函数,构造函数一般用于对类的成员进行初始化操作,析构函数则是进行类成员变量的回收工作。需要注意的是,构造函数声明方式的不同,将会造成不同的效果。
- 显示调用构造函数
- 隐式调用构造函数
- 使用匿名对象
class CVPractice
{
public:
CVPractice(cv::Mat img);
// 创建拷贝构造函数
CVPractice(const CVPractice& cvp);
~CVPractice();
void ChangeImage();
void showImage();
private:
s_image* simgPtr;
};
int main()
{
cv::Mat image = cv::imread("E:/Project Warehouse/C++Project/Test2/Test2/CVPractice/12.jpg", cv::IMREAD_COLOR);
// 隐式调用构造函数
CVPractice cvp(image);
// 显示调用构造函数
CVPractice cvp = CVPractice(img);
CVPractice* cvp = new CVPractice(img);
// 创建匿名对象,使用匿名对象直接调用类的方法
CVPractice(img).ShowImage();
}
”带参数的构造函数“和”匿名对象“的使用起来十分相似,需要加以区分!
- 带参数的构造函数实例化:
- 需要定义一个类,并在类中定义带参数的构造函数。
- 使用类名加上括号和参数来创建对象,并传入构造函数所需的参数。
- 对象的生命周期可以通过变量来控制,可以在程序的其他地方重复使用该对象。 - 匿名对象:
- 不需要定义类对象,可以直接使用类名加上括号和参数的方式,创建匿名对象,并传入构造函数所需的参数
- 匿名对象没有变量引用它,因此只能在创建的地方使用,不能在其他地方重复使用,并且匿名对象在创建时会被立即调用,调用后会被立即回收
2.拷贝构造函数(十分重要)
如果想用一个已经存在的对象创建一个新的对象,一般需要调用拷贝构造函数。C++的类一般默认会有一个拷贝构造函数,这种情况下用户不需要自定义就能完成对一个对象的copy工作。因此如果不希望对拷贝后的对象做任何额外操作的话,可以考虑使用默认的拷贝构造函数,即用户无需自定义。但是如果需要对拷贝后的对象做进一步的操作,则必须自定义一个拷贝构造函数;如果用户自定义了一个拷贝构造函数,编译器就不会再指定默认的拷贝构造函数。
以我们的项目为例,拷贝构造函数可以用于一个图像类对象的拷贝,即如果我现在有一个类对象,这个类对象中包含了一个关于图像信息的结构体指针,以及用于图像综合操作的成员函数;现在我希望给原图创建一份拷贝,并对拷贝后的图像进行图像预处理,但是要求又不能破坏原图,因此可考虑拷贝构造函数。
- 用拷贝构造函数对已经存在的对象进行拷贝
// 类名 新对象名(已存在的对象名);
// 该方法最常用
CVPractice cvp2(cvp);
// 类名 新对象名=已存在的对象名;
CVPractice cvp2 = cvp;
- 自定义拷贝构造函数
// 自定义拷贝构造函数语法格式:类名(const 类名& obj)
CVPractice(const CVPractice& obj);
3.使用拷贝构造函数构建图像副本
/* 拷贝构造函数示例 */
/* 该程序用于将一个图像类的对象进行拷贝,同时对拷贝后的图像进行图像预处理操作 */
/* CVPractice.h */
#ifndef CVPRACTICE_H
#define CVPRACTICE_H
#include<iostream>
#include "opencv2/opencv.hpp"
// 定义用户信息结构体
struct s_image
{
cv::Mat imgPtr;
int height;
int width;
};
class CVPractice
{
public:
CVPractice(cv::Mat img);
// 创建拷贝构造函数
CVPractice(const CVPractice& cvp);
~CVPractice();
void ChangeImage();
void showImage();
private:
s_image* simgPtr;
};
#endif // CVPRACTICE_H
/* CVPractice.cpp */
#include "cvpractice.h"
CVPractice::CVPractice(cv::Mat img)
{
this->simgPtr = new s_image;
this->simgPtr->imgPtr = img;
this->simgPtr->height = img.rows;
this->simgPtr->width = img.cols;
}
CVPractice::CVPractice(const CVPractice& cvp)
{
this->simgPtr = new s_image;
cvp.simgPtr->imgPtr.copyTo(this->simgPtr->imgPtr);
this->simgPtr->width = cvp.simgPtr->width;
this->simgPtr->height = cvp.simgPtr->height;
}
CVPractice::~CVPractice()
{
if(this->simgPtr != nullptr)
delete simgPtr;
}
void CVPractice::showImage()
{
std::cout << "height: " << this->simgPtr->height << std::endl;
std::cout << "width: " << this->simgPtr->width << std::endl;
std::cout << "ptr address: " << &this->simgPtr << std::endl;
std::cout << "address: " << this->simgPtr << std::endl;
cv::imshow("Image",this->simgPtr->imgPtr);
cv::waitKey(0);
}
void CVPractice::ChangeImage()
{
cvtColor(this->simgPtr->imgPtr,this->simgPtr->imgPtr,cv::COLOR_BGR2GRAY);
}
int main()
{
cv::Mat image = cv::imread("E:/Project Warehouse/C++ Project/Test2/Test2/CVPractice/12.jpg", cv::IMREAD_COLOR);
CVPractice cvp(image);
std::cout << "original" << std::endl;
cvp.showImage();
CVPractice cvp2(cvp);
cvp2.ChangeImage();
std::cout << "change" << std::endl;
cvp2.showImage();
return 0;
}
4.浅拷贝将导致重复析构问题
Tips:必须要注意,深浅拷贝影响的主要是堆区的数据,即new出来的指针变量,对于栈区的普通变量,则没有太大影响;因此为了确保程序内存的有效使用;对于new出来的指针变量必须使用深拷贝,而对于非new出来的栈区变量,则默认使用直接赋值的浅拷贝即可
C++默认的拷贝构造函数函数执行的是浅拷贝,或者用户自定义的拷贝构造函数中,如果只是采用直接赋值的方式,将会造成对象在析构时出现问题。假设有对象a和b,现在执行Class b(a),使用默认的拷贝构造函数将对象a的数据拷贝到对象b中;这一操作将造成对象b的指针直接指向了对象a所指的存储空间,即此刻两个对象所指的是同一块内存区域。在主程序执行完毕后将执行析构函数,析构函数将会先从对象b开始执行析构,程序将正常回收对象b的存储空间,但是等去析构对象a的时候,将会发生对已回收的存储空间再执行一次回收操作的危险(野指针)!这将会造成无可预知的后果。因此不建议使用默认的拷贝构造函数;需要自己去重写拷贝构造函数,重写的拷贝构造函数必须采用深拷贝