指针与函数


指针

指针是一个变量,其值为另一个变量的地址。

初始化

指针定义时尽量同时进行初始化,因为未初始化的指针容易出错;

int *ptrErr = {}; // 错误示例,没有赋初值
int *ptr = {nullptr};
int *p1, p2; // 声明创建了一个指针*p1与int变量p2,对每个指针变量,都需要一个'*';

指针的具体使用

  • 使用指针。& 可以应用在任何类型的变量上面,然后用变量对应的指针类型去存储变量的地址。间接运算符 * 可以获取内存地址所存储的对应的变量值,通常也称它为解引用运算符。当它和类型放在一起,例如 int*,便是声明指针的;当它和变量放在一起(前面没有加类型或者 auto),例如 *p_value,便是解引用的。
int *ptr2 = new int(); // 加了括号是将 'int' 初始化为 0;
int *ptr3 = new int;   // 未初始化的 int 可能会包含一个未定义的值
cout << *ptr2 << endl; // 0
cout << *ptr3 << endl; // 0,编译器或运行环境可能会在调试模式下或出于安全考虑将动态分配的内存初始化为 0
int value = 40;
int *ptr1 = &value;    // 地址运算符 "&" 可以获取变量的内存地址,常用于指针变量的初始化。
ptr2 = ptr1;           // 将value的地址传给了 ptr2,此时ptr1与ptr2都指向value的地址
*ptr2 = 60;            // ptr2 修改了value的值,*ptr1 = 60
  • 指针的算术运算:本质是让指针沿着一定的方向去移动指定大小(整数 * 指针所指向的类型大小)的单位。
float *ptr_f = new float[2]; // ptr_f 指向数组的首地址
ptr_f[0] = 20;
ptr_f[1] = 30;
// ptr_f + 1 在内存上沿着一定的方向移动指定大小:(1 * sizeof(float)) 的偏移量,
cout << *(ptr_f + 1) << endl; // 30
  • 特别注意:指向数值类型的指针必须解引用,才能拿到指针所指向的元素值。但是指向 char 类型的指针,可以不经过解引用,直接利用指针名获得元素的值。
const char *c_ptr = {"LSJune"};
std::cout << char_ptr << std::endl;        // LSJune
std::cout << *char_ptr << std::endl;       // L
std::cout << char_ptr[1] << std::endl;     // S
std::cout << *(char_ptr + 1) << std::endl; // S

指针与数组

  • 指针指向数组: 数组里面存的是变量,指针指向的是数组中的第一个元素。
float *f = new float[3]; // 分配数组,并将首地址存储在 f 中
f[0] = 2.0f;
f[1] = 4.0f;
std::cout << &f << std::endl;    // 0x7ffdc5cea698
std::cout << f << std::endl;     // 0x18bae70
std::cout << f + 1 << std::endl; // 0x18bae74
// 指针变量当数组来用
std::cout << f[0] << std::endl; // 2
std::cout << f[1] << std::endl; // 4
// 对数组指针解引用
std::cout << *f << std::endl;       // 2
std::cout << *(f + 1) << std::endl; // 4
delete[] f;
  • 指针数组:数组里面存的是变量的指针,这个变量可以是数组,也可以是变量。
float *fs[2];
fs[0] = new float[3];	// 变量是数组
fs[1] = new float();	// 变量是数值
fs[0][0] = 3.0f;
*fs[1] = 4.0f;
fs[0][1] = 5.0f;
std::cout << fs[0][0] << std::endl; // 3
std::cout << *fs[1] << std::endl;   // 4
std::cout << fs[0][1] << std::endl; // 5
delete[] fs[0]; 
delete fs[1];   

指针与const

  • 指向常量的指针:指针是变量(指向的地址可以变),指向的值是常量(常量值不可以修改)。
const int value = 10;
const int *ptr = &value;
cout << *ptr << endl; // 10
ptr = new int{20};    // 修改了指针指向的地址,但是不可以 *ptr = 20;
cout << *ptr << endl; // 20
  • 常量指针:存储的值为变量(存储的值可以修改),指针为常量(指向的地址不能变)。
int key = 10;
int *const p_key = &key;
cout << p_key << endl;  // 0x7fffdc0aa754
cout << *p_key << endl; // 10
key = 20;
cout << p_key << endl;  // 0x7fffdc0aa754
cout << *p_key << endl; // 20
  • 指向常量的常量指针:指针为常量,存储的值也为常量,指针地址与常量的值都不可以修改。
const int data = 10;
const int *const p_data = &data;
cout << p_data << endl;  // 0x7ffc0a7e47a4
cout << *p_data << endl; // 10

引用

概念与用法

  • 引用变量是一个别名,相当于是某个已存在变量的另一个名字。

  • 引用与指针的区别:

    1. 引用在创建时必须被初始化;指针可以在任何时间被初始化。
    2. 引用一旦被初始化,就不能再指向到另一个对象。指针可以在任何时候指向到另一个对象。
    3. 指针定义:类型名+’*’;引用定义:类型名+’&’。
  • 引用与函数:

    1. function(int param):按值传参,实际上传入的是原始变量的一个副本,修改参数不会修改原始变量的值。
    2. function(int &param):引用传参,实际上传入的是指向原始变量的一个指针,修改引用会修改原始变量的值,但是不会产生变量复制。
    3. function(const int &param):const 引用传参,向函数传入的是指向原始变量的一个指针,该指针是const类型,既可避免原始变量复制,同时又不会改变原始参数的值。
int value = 10;
int *ptr = &value;
int &ref = value;
*ptr += 10;
std::cout << "data of value: " << *ptr << std::endl;    // 20
std::cout << "data of value: " << ref << std::endl;     // 20
std::cout << "address of value: " << ptr << std::endl;  // 0x7ffcd9b4ea9c
std::cout << "address of value: " << &ref << std::endl; // 0x7ffcd9b4ea9c
ref += 10;
std::cout << "data of value: " << *ptr << std::endl;    // 30
std::cout << "data of value: " << ref << std::endl;     // 30
std::cout << "address of value: " << ptr << std::endl;  // 0x7ffcd9b4ea9c
std::cout << "address of value: " << &ref << std::endl; // 0x7ffcd9b4ea9c

注意:引用传参不能传表达式。如:

void refcube(int &a, int &b) {
    int temp;
    temp = a;
    a = b;
    b = temp;
}
int main() {
    int a = 10;
    int b = 20;
    refcube(a + 10, b);	// 报错:cannot bind non-const lvalue reference of type ‘int&’ to an rvalue of type ‘int’
    cout << a << " " << b << endl;
    return 0;
}

右值引用

  • 概念

    右值引用(Rvalue Reference)是 C++11 引入的一种新的引用类型,使用 && 语法表示。它可以绑定到临时对象(右值),例如字面常量、表达式的返回结果或者即将被销毁的对象。右值引用允许我们通过移动而不是拷贝的方式获取数据,减少不必要的资源消耗,提升程序性能。

  • 用法

    右值引用主要有两个用法:

    • 移动语义(Move Semantics):通过移动构造函数和移动赋值运算符,能够高效地转移资源(如内存、文件句柄等),避免昂贵的深拷贝操作。
    • 完美转发(Perfect Forwarding):通过模板函数,可以实现将参数原封不动地传递给另一个函数,既能保持参数的类型,也能保持参数的值类别(左值或右值)。
  • 移动语义

    移动语义的引入是为了优化拷贝操作。在传统 C++ 中,赋值操作通常涉及对象的深拷贝,这在处理大数据结构或资源管理时代价高昂。右值引用通过移动构造函数移动赋值运算符来避免这些开销。

    class MyClass {
    public:
        int* data;
        
        // 构造函数
        MyClass(size_t size) {
            data = new int[size];
            std::cout << "Constructor: Resource allocated." << std::endl;
        }
    
        // 析构函数
        ~MyClass() {
            delete[] data;
            std::cout << "Destructor: Resource deallocated." << std::endl;
        }
    
        // 拷贝构造函数
        MyClass(const MyClass& other) {
            data = new int[*other.data];
            std::cout << "Copy Constructor: Deep copy performed." << std::endl;
        }
    
        // 移动构造函数
        MyClass(MyClass&& other) noexcept {
            data = other.data;
            other.data = nullptr;  // 避免原对象的析构函数释放已转移的资源
            std::cout << "Move Constructor: Resource moved." << std::endl;
        }
    
        // 移动赋值运算符
        MyClass& operator=(MyClass&& other) noexcept {
            if (this != &other) {
                delete[] data;  // 释放已有资源
                data = other.data;
                other.data = nullptr;  // 防止释放已转移的资源
                std::cout << "Move Assignment Operator: Resource moved." << std::endl;
            }
            return *this;
        }
    };
    • 移动构造函数:接受一个右值引用参数 MyClass&&,直接窃取(移动)资源,避免拷贝。

    • 移动赋值运算符:和移动构造函数类似,但需要先释放自身资源,再从另一个对象中移动资源。

  • 完美转发

    完美转发是指在模板函数中,将参数完整地传递给另一个函数,同时保留参数的类型(左值或右值)和原有的属性(如 constvolatile)。为此,C++11 引入了 std::forwardstd::move

    #include <iostream>
    #include <utility>  // std::forward
    
    // 接收右值引用并完美转发的模板函数
    template <typename T>
    void wrapper(T&& arg) {
        // 将参数完美转发给被调用的函数
        callee(std::forward<T>(arg));
    }
    
    void callee(int& x) {
        std::cout << "Lvalue reference: " << x << std::endl;
    }
    
    void callee(int&& x) {
        std::cout << "Rvalue reference: " << x << std::endl;
    }
    
    int main() {
        int a = 10;
        wrapper(a);    // 左值传递
        wrapper(20);   // 右值传递
        return 0;
    }

    std::forward(arg):根据 T 的类型正确地转发参数。T 是左值引用时,arg 被转发为左值;T 是右值引用时,arg 被转发为右值。

    std::forward:用于完美转发,根据传递给模板函数的参数类型,来正确地转发左值或右值。

    std::move:简单地将一个左值强制转换为右值引用,不检查类型。

    std::move:用于将一个对象显式转换为右值引用,使之适用于移动语义。

  • 误区

    误区 1:右值引用仅用于临时对象。实际上,右值引用用于所有临时对象以及可以“被窃取”的对象。

    误区 2:右值引用总是比左值引用更高效。右值引用的高效依赖于具体场景和正确的实现,并非总是如此。

函数指针

与数据项相似,函数也有地址。函数的地址是存储其机器语言代码的内存的开始地址。

初始化与调用

要再 postProcess() 函数里面调用 NMS() 函数,通常需要完成下面的工作:

  1. 获取函数的地址:使用函数名(后面不跟参数)即可获取函数地址;
postProcess(NMS);	// 将NMS函数传入postProcess,使得postProcess可以在内部调用NMS函数
postProcess(NMS());	// 首先执行NMS()函数,然后将NMS()函数的返回值当作参数传给postProcess函数
  1. 声明函数指针:声明指向函数的指针时,必须指定指针指向的函数类型(函数的返回类型以及参数列表)。
int NMS(std::vector<int>);    // 函数原型
int (*nms)(std::vector<int>); // 声明函数指针,只需用 `(*函数指针名)` 要替换函数名即可
// void *nms(std::vector<int>); 返回值为指针的函数,函数名为 nms,并非函数指针。

通常,要声明指向特定类型的函数的指针,可以首先编写这种函数的原型,然后用(*pf)替换函数名。这样pf就是这类函数的指针。

  1. 函数指针初始化
int NMS(std::vector<int>); // 函数原型
int (*nms)(std::vector<int>){NMS}; // 用函数名初始化函数指针
auto nms = NMS;		// 用关键字 auto 初始化函数指针
auto *nms = NMS;	// 显式初始化,代码可读性更强
auto *nms = &NMS;	// 用地址运算符 & 初始化函数指针
auto nms = &NMS;	// 显式初始化,代码可读性更强
  1. 函数指针调用函数
int (*nms)(std::vector<int>){NMS}; // 函数指针初始化
int x = NMS({});    // 函数原型调用
int y = nms({});    // 函数指针隐式调用
int z = (*nms)({}); // 函数指针显式调用

函数指针数组

假设现在声明了三个函数原型:

const double* f1(const double ar[], int n);
const double* f2(const double [], int n);
const double* f3(const double *, int n);

这几个函数的参数看似不同,但实际上相同;将其初始化为函数指针数组:

const double* (*pf[3])(const double *, int) = {f1, f2, f3};
// 指针调用
const double* px = *pf[0](param1, param2);	// 隐式调用
const double* py = (*pf[1])(param1, param2);// 显式调用
// 解引用,获得指向double的值
double x = *pf[0](param1, param2);	// 隐式调用
double y = *(*pf[1])(param1, param2);	// 显式调用

运算符[]的优先级高于 *,因此 *pf[3] 表明 pf 是一个包含三个指针的数组。此处不能使用auto,自动类型推断只能用于单值初始化,而不能用于初始化列表

如何显示的声明指向 pf 数组的指针:

// 'const double *' 返回值类型,
const double* (*(*p_pf)[3])(const double *, int) = &pf;	// 运算符 [] 与 () 同级。
auto p_pf = &pf; // 这样也可以
// 指针调用
const double* px = (*p_pf)[2](param1, param2);	// 隐式调用
const double* py = (*(*p_pf)[1])(param1, param2);	// 显式调用
// 解引用,获得指向double的值
double x = *(*p_pf)[2](param1, param2);	// 隐式调用
double y = *(*(*p_pf)[1])(param1, param2);	// 显式调用
  • 数组 pf&pf 之间的区别:
    1. pf 表示数组第一个元素的地址,但 &pf 表示整个数组(三个指针块的地址),即:pf=&pf[0]
    2. 数值上来讲,pf&pf 值相同,但是他们的类型不同,pf+1 为数组中的下一个元素,&pf+1 为数组 pf 后面一个12字节(假设 pf 数组大小为12字节,每个元素4字节,三个元素)内存块的地址。
    3. 解引用次数不同。要得到第一个元素的值,pf 需要解引用一次,但 &pf 需要解引用两次:*pf == **&pf == pf[0]

typedef、using简化

利用 typedef 或者 using 关键字创建类型别名:

typedef const double* (*p_f)(const double *, int);	// p_f 是一个类型别名
using p_f = const double* (*)(const double *, int);	// 使用using创建类型别名

使用类型别名简化代码

p_f pf[3] = {f1, f2, f3};	// 函数指针数组
p_f (*p_pf)[3] = &pf;	// 函数指针数组的指针

函数模板

模板概念与定义

函数模板是通用的函数描述,它们使用泛型来定义函数,其中的泛型可用具体的类型(如 int 或 double)替换。模板的定义以关键字 template 开始,后跟一个由尖括号 <> 括起来的模板参数列表。定义模板参数的关键字 typename

template <typename T> // template <class T>
void func(T t) {
    // code
}

基于函数模板生成的函数定义被称为模板的一个实例。

模板实例化与具体化

编译器使用模板为特定类型生成函数定义时,得到的是模板实例(instantiation)。模板并非函数定义,但使用 int 的模板实例是函数定义。这种实例化方式被称为隐式实例化

template <typename T> // template <class T>
T add(T t1, T t2) {
    return t1 + t2;
}

template float add<float>(float, float);	// 显式实例化声明和定义合并

int test(){
    int tmp1 = add(2, 3);					// 隐式实例化
	double tmp2 = add(2.3, 3.2);
    cout << add<float>(2.2, 3.3) << endl;	// 显式实例化
    float f1 = 2.1f;
    float f2 = 2.2f;
    cout << add(2.2, 3.3) << endl;	 		// 也是显式实例化调用

显式具体化的原型和定义应以 template <> 打头,并通过名称来指出类型。空的尖括号 <> 表示编译器不需要做类型推导。函数模板具体化的定义必须放在函数模板的声明和定义之后。函数模板具体化的定义需要传递具体的参数类型。

template <typename T> // template <class T>
T add(T t1, T t2) {
    return t1 + t2;
}

// template <> double add<double>(double t1, double t2);	// 声明1
// template <> double add(double t1, double t2); 		  	// 与声明1等价

template <> double add<double>(double t1, double t2){	
	cout<< "double"<<endl;
	return t1 + t2;
}

void test(){
    int tmp1 = add(2, 3);
	double tmp2 = add(2.3, 3.2);	// 显式具体化
	float f1 = 2.1f;
	float f2 = 2.3f;
	cout << add<float>(f1,f2) <<endl;
	double d1 = 2.0;
    double d2 = 3.0;
	cout << add(d1,d2) <<endl;		// 显式具体化
}

调用优先顺序:非模板函数 > 具体化模板函数 > 原始模板函数 。

模板参数

模板参数分两种:类型模板参数、非类型模板参数

template <typename T, int N>
T func(const T (&array)[N]){	// 传参为任意数据类型任意大小的数组。
    // code
};

其中 typename T 为类型模板参数,经过实例化会变成具体类型。int N 为非类型模板参数,经过实例化会变成具体的值。

当模板参数列表中,同时有类型模板参数和非类型模板参数时,建议将非类型模板参数写在类型模板参数的前面。

指定类型的模板参数,可以用具体的数据类型为模板参数指定默认值。

template <typename T1=float, typename T2> 
void add(T1 t1, T2 t2) {
    // code
}

可变参数

  1. 概念与定义。

    可变参数的含义是:在函数传参的时候,参数的数量、类型都是可变的,不确定的。

    可变参数模板是支持任意数量和类型的参数的类模板或函数模板。要创建可变参数模板,需要理解几个要点:模板参数包、模板参数包、展开参数包;参考下例:

    template <typename... _Args> 
    void func(_Args... _args);
    • 在定义可变参数函数时,使用省略号 ... 表示参数是可变的。
    • 其中 _Args 是一个模板参数包,_args 是一个函数参数包;
  2. 如何访问参数包的内容?

    访问参数包内容时,不能用 _Args[idx] 或者 _args[idx] 来访问第 idx 个类型或者参数,因为索引功能在此处不适用。可以通过递归或者数组来访问参数包内容。

    • 递归访问

      template <typename... _Args> 
      void show_list(_Args... _args){
          show_list(args...);
      }

      这样访问会报错,比如传入参数为:show_list(3, "hello", 3.33); 如此会一直调用下去,陷入死循环,直至程序崩溃。应该改为这样:

      template <typename T, typename... _Args> 
      void show_list(T t, _Args... _args){
          cout << t << endl;
          show_list(args...);
      }
      
      void show_list(){}

      这样改写之后,首先传入show_list(3, "hello", 3.33); 其中 t = 3, _args = ("hello", 3.33),再次调用时则为 show_list("hello", 3.33); 其中 t = "hello", _args = (3.33),最后当参数为空时,调用 void show_list(){} 函数结束循环。也可以定义打印参数的模板:

      template <typename T> 
      void show_list(T t){
          cout << t << endl;
      }
      
      template <typename T, typename... _Args> 
      void show_list(T t, _Args... _args){
          show_list(args...);
      }
    • 通过数组获取所有参数

      template <typename... _Args>
      int offset(int index, _Args &&..._args) const {
          const int index_array[] = {index, _args...};
          return offset_array(sizeof...(_args) + 1, index_array);
      }
      
      int offset_array(size_t size, const int *index_array) const;
      • int index:第一个参数是一个 int 类型的参数,命名为 index_Args&&... index_args:这是一个函数参数包,_Args 是模板参数,_Args&&... 是其右值引用形式。该参数包名为 index_args,允许传入任意数量的参数,并将它们全部作为右值引用进行接收。
      • 定义了一个 const int 数组,数组元素由参数包 index_args... 展开填充,其中第一个元素为indexindex_args... 会将所有传入的参数展开为一个逗号分隔的列表,最终结果是一个包含所有参数的 int 数组。
      • sizeof...() 运算符可以返回参数包中的元素数量。

引用传参

  • 按值传参。
template <typename T> 
T add(T t1, T t2) {
    return t1 + t2;
}

void test(){
    int t1 = 2;
    double t2 = 3.6;
    int& t3 = t1; 
    double& t4 = t2
    cout << add(t1, t1) << endl;
 	cout << add(t1, t3) << endl;	// 引用类型在模板推导时会被看作它所引用的类型,即 int。
    // cout << add(t1, t2) << endl; 报错,因为模板要求两个参数类型相同,如果要传入不同类型的参数,需要用显式实例化,进行类型强转
    cout << add<double>(t1, t2) << endl;
    // cout << add(t1, t4) << endl;	// 报错,因为第二个形参的类型为 double&,不能指向 int 变量 t1。
	cout << add<double>(t1, t4) << endl; // 类型强转了
}
  • 引用传参
template <class T> 
T add(T &t1, T& t2) {
    return t1 + t2;
}

template <> double add(double& d1 , double& d2){
	return d1 + d2;
}

void test(){
    int t1 = 2;
    double t2 = 3.6;
    // 报错:因为第一个形参的类型为 double&,不能指向 int 变量 t1
    cout << add<double>(t1,t2) <<endl;
}

返回类型

当传入的参数类型不同,存在强制转化时,如何确定变量类型?如下例:

template <class T1, class T2> 
void add(T& t1, T& t2) {
    type t = t1 + t2;	// t 应该是什么类型,如 T1 为 int 类型,T2 为 double 类型
}

利用关键字 decltype 确定变量类型。decltype 可以传入表达式,如:

template <class T1, class T2> 
void add(T &t1, T& t2) {
    decltype(t1 + t2) t = t1 + t2;	
}

但是,如果函数需要添加返回类型时,又该如何定义,如下例:

template <class T1, class T2> 
type add(T &t1, T& t2) {
    decltype(t1 + t2) t = t1 + t2;	
    return t;
}

不可以直接用 decltype(t1 + t2) 作为返回类型,因为此时还未声明参数 t1t2,它们不在作用域内,必须在声明参数后使用 decltype。此时可以利用关键字 auto 去推断返回值类型。如:

template <class T1, class T2> 
auto add(T &t1, T& t2) {
    decltype(t1 + t2) t = t1 + t2;	
    return t;
}

但是,使用auto来推导函数的返回值类型时,会默认去掉引用和 const 限定符,因此,以上方式会导致返回值发生不必要的复制。为了让返回值被 const 修饰,且采取引用的方式来传值,需要显式地加上 const &,上述代码可以改为:

template <class T1, class T2> 
const auto& add(T &t1, T& t2) {
    decltype(t1 + t2) t = t1 + t2;	
    return t;
}

也可以利用 decltype 关键字,decltype 可以起到与 const auto& 相同的作用:

// 方式1.拖尾方式:decltype(返回值相关代码)
template <typename T1, typename T2>
auto larger(T1 t1, T2 t2) -> decltype(t1 + t2) {
    return t1 + t2;
}
// 方式2.与auto关键字结合:decltype(auto)
template <typename T1, typename T2>
decltype(auto) larger(T1 t1, T2 t2) {
    return t1 + t2;
}

文章作者: LSJune
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 LSJune !
评论
  目录