网站服务器迁移步骤,做外贸网站特色,php 关闭网站,微信wordpress小程序1 指针的基础概念
指针是 C 的核心之一#xff0c;使用 C 语言构建的程序之所以性能强悍#xff0c;有很大部分原因是体现在使用指针直接操作内存。当然这样的工具是一把双刃剑#xff0c;错误的指针操作可能会导致程序崩溃或者数据损坏。 指针主要有四个方面的用途#x…1 指针的基础概念
指针是 C 的核心之一使用 C 语言构建的程序之所以性能强悍有很大部分原因是体现在使用指针直接操作内存。当然这样的工具是一把双刃剑错误的指针操作可能会导致程序崩溃或者数据损坏。 指针主要有四个方面的用途 1动态内存分配使用 new 操作符在堆上分配内存。 2传递数据通过指针传递大型数据对象可以显著提高程序的效率比如使用指针作为函数参数。 3回调函数指针可以用于传递函数的地址函数式编程正是建立在这个功能的基础上。 4优化性能指针可以直接访问内存避免了一些额外的开销如复制数据或者查找数据等。 指针本身是一个变量其值为另一个变量的内存地址因此要掌握指针的原理与作用需要从理解内存地址开始。
1.1 内存地址
内存地址是指计算机内存中存储变量或对象的地址。内存空间大小就是寻址能力即能访问到多少个地址比如 32 位机器内存空间大小就是 2^32 4294967296也就是 4 GB 。每个变量或对象在内存中都有一个唯一的地址通过该地址可以访问和操作该变量或对象。注意一 个内存地址对应一个字节以 int 类型的变量为例其占据 4 个内存地址其中首个内存地址就是这个变量的地址。
#include iostreamint main()
{int vals[4]{};printf(val1 address %p\n, vals[0]);printf(val2 address %p\n, vals[1]);printf(val3 address %p\n, vals[2]);printf(val4 address %p\n, vals[3]);return 0;
}上面代码的输出为
val1 address 0000005420F6F978
val2 address 0000005420F6F97C
val3 address 0000005420F6F980
val4 address 0000005420F6F984为了能够说明 1 个 int 类型的变量占据 4 个内存地址我们在上面的代码中使用占据连续内存的数组来做测试由这个输出可以看出数组 vals 的第一个元素所占据的内存地址由 0000005420F6F978 到 0000005420F6F97B 再往下的一个地址就是第二个元素的首地址 0000005420F6F97C刚好是 4 个内存地址其首个内存地址 0000005420F6F978 就是这个数组 vals 的第一个元素的地址同时也是这个数组变量 vals 的地址。
1.2 指针是什么
指针是一种变量它存储的是其他变量的内存地址。通过指针我们可以间接地访问和操作存储在内存中的变量。 由这个定义可知指针既然是一个变量那么它本身也需要占用内存即有自己对应的内存地址。如下为样例代码 x64 平台编译
#include iostreamint main()
{void* ptr nullptr;printf(ptr address %p\n, ptr);printf(ptr address size %llu\n, sizeof(ptr));printf(ptr value %p\n, ptr);return 0;
}上面代码的输出为
ptr address 000000196B5CF678
ptr address size 8
ptr value 0000000000000000其中指针 ptr 虽然指向的是一个空地址但是其作为一个变量依然有自己的内存地址000000788E9AF638。另外ptr address size 8 表明使用 x64 平台编译时指针所占用的内存大小为 8 个字节 32 位平台编译是 4 个字节刚好可以保存一个内存地址这就是指针能够存储其他变量的内存地址的原理。
2 指针的基本使用
指针是一种变量所以在使用前和其他类型变量一样也需要定义与初始化指针变量定义时前面会有一个星号*。例如int *ptr; 意思是定义了一个指向整数的指针。指针变量在使用之前必须被初始化否则其值是未定义的这个时候指向的是一个随机的内存地址对其操作很容易引起程序崩溃。通过在指针变量前加上星号*可以访问指针所指向的对象相当于操作这个对象本体。
2.1 指针的定义与初始化
指针的定义和初始化可以通过以下方式完成
#include iostreamint main()
{int val 1; // 定义一个整型变量 val并初始化为 1 int *ptr val; // 定义一个指向整型的指针 ptr并将它初始化为变量 val 的地址printf(val address %p\n, val);printf(ptr address %p\n, ptr);printf(ptr value %p\n, ptr);return 0;
}上面代码的输出为
val address 00000035076FFCB4
ptr address 00000035076FFCD8
ptr value 00000035076FFCB4其中指针 ptr 的值等于整型变量 val 的地址。第 6 行 int *ptr val; 中的 符号是取地址符用于获取变量的内存地址。基本类型 int 、float 等、结构体类型 struct 以及类类型 class 的地址获取都需要使用该符号。
2.2 解引用
*操作符是 C 的解引用操作符用于获取指针所指向的对象对其操作相当于对指针所指向对象的操作 指针的定义和初始化可以通过以下方式完成
#include iostreamint main()
{int val1 1; int *ptr val1; *ptr 2; //该表达式相当于 val1 2; int val2 *ptr; //该表达式相当于 int val2 val1; printf(val1 %d\n, val1);printf(val2 %d\n, val2);return 0;
}上面代码的输出为
val1 2
val2 2其中*ptr 在上面程序的运行过程中就是整型变量 val1 不管是对其做赋值操作*ptr 2;还是将其用于其他变量的初始化int val2 *ptr;都相当于直接操作整型变量 val1 自身。 对于结构体和类一般是使用箭头操作符 - 来操作对象的成员变量或者成员函数但是根据前面所描述的解引用概念使用解引用操作符也可以起到相同作用
#include iostream
#include stringusing namespace std;class Student
{
public:Student() {};Student(string name):m_name(name) {};~Student() {};public:string getName(){return m_name;}private:string m_name;
};int main()
{Student st(zhangsan);Student *ptr st;string name1 st.getName();string name2 ptr-getName();string name3 (*ptr).getName(); //使用解引用操作符return 0;
}注意上面的语句 string name3 (*ptr).getName();同样可以调用对象 st 的成员函数。只是由于 C 提供了更方便的箭头操作符 - 所以一般我们才不会如此使用。
2.3 指向数组的指针
指向数组与上面章节的指向基本类型 int 、float 等、结构体类型 struct 以及类类型 class 的使用方式有所不同数组名本身就是数组的首地址所以无需做取地址操作如下为样例代码
#include iostreamint main()
{int vals[6] { 1,2,3,4,5,6 };printf(%p \n, vals);printf(%p \n, vals);int* ptr vals;for (size_t i 0; i 6; i){printf(%d , *(ptri));}return 0;
}上面代码的输出为
vals address 000000C03976F8C8
vals address 000000C03976F8C8
1 2 3 4 5 6从上面输出可以看出数组名 vals 与对数组名取地址 vals 所得到的内存地址是一样的所以如果用指针指向某个数组直接将数组名赋值给指针即可。第 13 行 printf(%d , *(ptri)); 中的 *(ptri) 是指针的运算在下面章节会详细讲解。 指针不光可以指向整个数组还可以指向数组中的某一个元素如下
#include iostreamint main()
{int vals[6] { 1,2,3,4,5,6 };printf(before modification, vals[2] %d \n, vals[2]);int* ptr vals[2]; //指向的数组第 3 个元素*ptr 10; //将其所指向的数组第 3 个元素的值修改为 10printf(after modification, vals[2] %d \n, vals[2]);return 0;
}上面代码的输出为
before modification, vals[2] 3
after modification, vals[2] 10注意第 10 行 int* ptr vals[2]; 这里是指向数组里面的一个整型元素所以一定要用取地址操作符。
2.4 指向函数的指针
C中的函数也有地址调用函数的本质就是跳转到这个函数的地址然后执行里面的函数体。因此可以声明指向函数的指针并使用这个指针调用函数。指向函数的指针也被称作是函数指针其定义方式为
函数返回值类型 (* 指针变量名) (函数参数列表);函数返回值类型表示该指针变量所指向函数的返回值类型。 指针变量名表示该指针变量的名称。 函数参数列表表示该指针变量所指向函数的参数列表。 为了使用方便一般会用关键字 typedef 来定义函数指针即typedef 函数返回值类型 (* 指针变量名) (函数参数列表) 。例如
typedef int (*ADD)(int,int);
ADD addFunc;使用这种方式可以目标函数看作为一个类型然后再用它去定义指针增强复用性。 对于无参数或者无返回值的函数需要使用用 void 关键字例如
typedef void (*TESTFUNC)(void); //无参数和返回值2.4.1 指向全局函数的函数指针
以如下代码为例
#include iostreamint add(int a, int b)
{int sum a b;return sum;
}int main()
{typedef int(*ADDFUNC)(int, int);ADDFUNC f1 add;int sum1 f1(1, 2); //直接使用函数名int sum2 (*f1)(1, 2); //取函数地址printf(sum1 %d\n,sum1);printf(sum2 %d\n, sum2);return 0;
}上面代码的输出为
sum1 3
sum2 3特别注意的是因为函数名本身就可以表示该函数地址指针因此在获取函数指针时可以直接用函数名也可以取函数的地址。因此上面代码中 int sum1 f1(1, 2); 以及 int sum2 (*f1)(1, 2); 作用是相同的。
2.4.2 指向对象成员函数的函数指针
以如下代码为例
#include iostreamclass MyAdd
{
public:MyAdd() {}~MyAdd() {}public:int add(int a, int b){int sum a b;return sum;}};int main()
{MyAdd myAddObj;typedef int(MyAdd::*ADDFUNC)(int, int);ADDFUNC f1 MyAdd::add;int sum (myAddObj.*f1)(1, 2);printf(sum %d\n, sum);return 0;
}上面代码的输出为
sum 3注意对象的成员函数属于类所以其存储位置在对象外的空间中由所有的类对象共享。因此 MyAdd 类中的 add() 成员函数不是属于 myAddObj 对象的而是属于 MyAdd 类。所以使用 类名::成员函数名 的形式将该成员函数赋给函数指针。
2.4.3 回调函数
回调函数是函数指针的一个重要应用场景比如在使用 C 的容器类时经常会自定义回调函数用以实现定制化功能。以 vector 的自定义排序为例代码如下
#include iostream
#include vector
#include algorithmusing namespace std;struct Student
{string id;double score;
};bool compareByScore(Student stu1, Student stu2)
{return stu1.score stu2.score;
}int main()
{vectorStudent students;students.emplace_back(Student{ s1,98.2 });students.emplace_back(Student{ s2,97.6 });students.emplace_back(Student{ s3,92.8 });students.emplace_back(Student{ s4,95 });students.emplace_back(Student{ s5,99 });printf(before sort\n);for (size_t i 0; i students.size(); i){printf(%s(%lf) , students[i].id.c_str(), students[i].score);}printf(\n);sort(students.begin(), students.end(), compareByScore);printf(after sort\n);for (size_t i 0; i students.size(); i){printf(%s(%lf) , students[i].id.c_str(), students[i].score);}printf(\n);return 0;
}上面代码的输出为
before sort
s1(98.200000) s2(97.600000) s3(92.800000) s4(95.000000) s5(99.000000)
after sort
s3(92.800000) s4(95.000000) s2(97.600000) s1(98.200000) s5(99.000000)其中函数 compareByScore 便作为一个函数指针的入参传递给函数 sort 。
2.4.4 函数指针和指针函数的区别
函数指针和指针函数是两种不同的编程概念前者是一个指针后者是一个函数除了名字比较容易混淆实际上是完全不同的概念。 上面内容已经说明了函数指针的含义与作用指针函数的定义如下 1指针函数本身就是一个函数其返回的类型是指针。 2指针函数用于返回指针类型的值例如动态分配的对象或数组的指针。
2.5 指向指针的指针
指针可以指向所有数据类型的变量基本类型、结构体类型、类类型等而指针自身也是一种变量所以指针自然也可以指向指针。把指向指针的指针理解透彻基本上也就能掌握了指针的精髓。如下为样例代码
#include iostreamint main()
{int val1 1;int *ptr1 val1;int **ptr2 ptr1;printf(ptr1 address %p\n, ptr1);printf(ptr1 address %p\n, (*ptr2));printf(ptr2 value %p\n, ptr2);return 0;
}上面代码的输出为
ptr1 address 000000C4A839F758
ptr1 address 000000C4A839F758
ptr2 value 000000C4A839F758由结果可以看出指向指针的指针变量 ptr2 保存了指针变量 ptr1的地址 000000C4A839F758 。 其中代码第 10 行 int **ptr2 ptr1; 定义了一个指向指针的指针这里用了两个星号*其保存的值就是指针变量 ptr1的地址。 第 11、 12、 13 行代码尤为重要 第 11 行代码 printf(ptr1 address %p\n, ptr1); 其中的 ptr1 是对指针变量 ptr1 做取地址操作。 第 12 行代码 printf(ptr1 address %p\n, (*ptr2)); 其中的 (*ptr2) 是对指针变量 ptr2 做解引用操作再对其做取地址操作相当于直接对指针变量 ptr1 做取地址操作。 第 13 行代码 printf(ptr2 value %p\n, ptr2); 对指向指针的指针取值直接用其变量名即可。
2.6 创建动态内存
使用指针可以在堆中创建内存空间先在堆中申请一块内存空间然后将其首地址返回给一个指针后面通过该指针便可读写这一块内存其创建和销毁过程都需要手动控制。 C 使用 new 或者 new[] 操作符在堆中创建一块内存空间使用 delete 或者 delete[] 释放这块申请的内存空间。如下
int* ptr new int;这一行代码定义了一个指向整型的指针变量 ptr 并且使用 new 操作符在堆中创建一个 int 类型的内存空间并将该空间首地址返回指针变量 ptr 。 如果想将这个内存空间赋值为 1 可以做如下操作
*ptr 1;在使用完这个内存空间后一定要将其释放避免内存泄露并且将指针变量 ptr 赋值 nullptr避免悬垂指针它所指向的内存空间已经被释放
delete ptr;
ptr nullptr;注意释放的操作不能再次执行如果再做一次 delete ptr; 则会导致程序崩溃。
2.7 指针的运算
指针为什么一定要定义类型即使无类型也需要使用 void 做定义这个要求的一个来源就是指针运算需要按照类型做处理
#include iostreamint main()
{int val1 1;short val2 2;int *ptr1 val1;short *ptr2 val2;printf(before adding, ptr1 value %p\n, ptr1);printf(before adding, ptr2 value %p\n, ptr2);ptr1;ptr2;printf(after adding, ptr1 value %p\n, ptr1);printf(after adding, ptr2 value %p\n, ptr2);return 0;
}上面代码的输出为
before adding, ptr1 value 0000000BBC54F614
before adding, ptr2 value 0000000BBC54F634
after adding, ptr1 value 0000000BBC54F618
after adding, ptr2 value 0000000BBC54F636从上面代码运行的结果可以看出不同类型的指针变量其运算的步长由其类型确定。 int 类型的指针变量对其做 操作后该变量的值增加了 4 指向下一个 int 变量。 short 类型的指针变量对其做 操作后该变量的值增加了 2 指向下一个 short 变量。
2.7.1 指针的加减运算
指针的加减运算通常用于对数组的操作如下为样例代码
#include iostreamint main()
{int vals[6] { 1,2,3,4,5,6 };int* ptr vals;for (size_t i 0; i 6; i){printf(%d , *(ptr i));}return 0;
}上面代码的输出为
1 2 3 4 5 6上面代码的核心语句是 printf(%d , *(ptr i));其中 ptr i 是指向数组中的第 i 个元素的地址再加上前面的星号 * 则完成了对其的解引用操作最终获取到了对应数组元素的值。
2.7.2 指针的赋值操作
指针的赋值操作也是一个在开发中常见的操作。其作用是将一个指针的值这个值是内存中某一个变量的地址赋给另一个指针。如下为样例代码
#include iostreamint main()
{int val1 1;int *ptr1 val1;int *ptr2 ptr1;printf(ptr1 value %p\n, ptr1);printf(ptr2 value %p\n, ptr2);return 0;
}上面代码的输出为
ptr1 value 0000006FCF3CFBC4
ptr2 value 0000006FCF3CFBC4赋值操作后 指针变量 ptr2 的值就等于指针变量 ptr1 的值。
3 使用指针的注意点
3.1 常量指针与指针常量
常量指针const pointer和指针常量pointer to const是两个不同的概念常量指针指的是其指向变量的值不可改变但是指针本身是可以改变的可以指向其他变量指针常量指的是指针本身是常量其不可以再指向其他变量。 常量指针的样例代码
const int val1 1;
int *ptr1 val1; //错误必须使用常量指针
const int *ptr1 val1; //OK
*ptr1 2; 指针常量的样例代码
int val1 1;
int val2 2;
int const *ptr1 val1; //OK
*ptr1 val2; //错误指针本身是常量其不可以再指向其他变量。3.2 使用 nullptr
前面章节的代码中多处使用了 nullptr 关键字该关键字是在 C11 标准中引入的用于表示空指针。在 C11 及以后的版本中nullptr 替代了 C98/03 中的 NULL 或 0 作为空指针的表示。该关键字可以避免函数重载问题如下为样例代码
void overLoadFunc(int* val);
void overLoadFunc(int val);int main()
{overLoadFunc( NULL ); // 期待调用 overLoadFunc(int* val); 但实际调用却是 overLoadFunc(int val);
}上面代码中的 overLoadFunc( NULL ); 实际调用的是 overLoadFunc(int val); 。其原因是 NULL 本身就是整数 0 因此进入了整型参数的重载函数。
3.2 野指针出现的原因
野指针出现的原因主要有以下三种 1指针变量未初始化。局部指针变量的默认值是一个随机值如果此时访问该指针则会引起程序崩溃。所以指针变量在创建的同时应当被初始化要么将指针设置为 nullptr 要么让它指向合法的内存 new 出来的对象或者现有的一个对象。 2释放内存后没有将指针设置为 nullptr 。不管是 free 还是 delete 在释放内存时只是把指针所指的内存给释放掉了但此时指针的值依然是之前内存空间的首地址。此时访问该指针则会引起程序崩溃。 3指针操作超越变量作用范围。栈内存在函数结束时会被释放如果将其内存地址通过指针返回给调用者此时再访问则会引起程序崩溃。