【C++高级编程】第一弹

C++高级编程笔记

函数指针和回调函数

引用

构造函数和匿名对象

this指针和智能指针

继承和多态性

容器STL

多线程和线程锁

函数指针和回调函数:

1. 何为函数指针?

函数指针,即指向函数的指针变量。作为一个指针变量,它同普通的指针变量没有什么区别,都满足指针变量使用的三个基本操作。可以使用函数指针直接调用函数,将函数指针作为另一函数的形参(回调函数),亦可将其作为函数的返回值使用。

2.指针变量的定义

需要注意的是,必须要给指针变量名加上括号,否则其表示的是一个指针类型的返回值;同时函数参数列表不写形参名!

typedef 函数返回值类型 (*指针变量名) (函数参数列表);

// 函数指针变量为Fun1,其函数有一个int类型的形参,且返回值也是int类型
typedef int (*Fun1)(int);
// 函数指针变量为Fun2,其函数无形参,且无返回值
typedef void (*Fun2)(void);
// 函数指针变量为Fun1,其函数无形参,且返回值也是自动类型
typedef void* (*Fun3)(void*);

3.何为回调函数?

回调函数就是一个通过函数指针调用的函数。把被调用函数的指针作为参数,传递给另一个函数的形参,那么这个被调用的函数就是回调函数。回调函数往往不是被直接调用,通常回调函数的函数名会被传递给一个回调注册函数,通过回调函数的注册,就能得到指向这个函数的函数指针,再利用该函数指针去调用相应的回调函数

回调函数在开发中的使用场景,通过是用于SDK的使用,它将调用者和被调用者分开。以我们线阵相机的使用为例,线阵的取图是使用了海康SDK中的回调取图方式,即通过注册一个自定义的回调函数,相机在采集取图后会自动调用该回调函数,往回调函数的形参中传递图像的相应参数。这样一来,我只关心回调函数的操作即可,我无需考虑相机采集转码的具体操作;利用回调函数就将相机复杂的底层操作,同我对图像进行的后处理操作分隔开来。

img

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,函数能正确生成并使用临时变量

在这里插入图片描述

如果函数形参没有指定为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();
}

”带参数的构造函数“和”匿名对象“的使用起来十分相似,需要加以区分!

  1. 带参数的构造函数实例化:
    - 需要定义一个类,并在类中定义带参数的构造函数。
    - 使用类名加上括号和参数来创建对象,并传入构造函数所需的参数。
    - 对象的生命周期可以通过变量来控制,可以在程序的其他地方重复使用该对象。
  2. 匿名对象:
    - 不需要定义类对象,可以直接使用类名加上括号和参数的方式,创建匿名对象,并传入构造函数所需的参数
    - 匿名对象没有变量引用它,因此只能在创建的地方使用,不能在其他地方重复使用,并且匿名对象在创建时会被立即调用,调用后会被立即回收

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的时候,将会发生对已回收的存储空间再执行一次回收操作的危险(野指针)!这将会造成无可预知的后果。因此不建议使用默认的拷贝构造函数;需要自己去重写拷贝构造函数,重写的拷贝构造函数必须采用深拷贝
在这里插入图片描述

  • 8
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值