#内存、地址、指针
计算机里数据存储在内存单元,而每一个内存都有唯一的编号,这个编号也叫地址,在C语言里,一个指针就相当于一个地址,用户可以通过指针准确的访问一块内存的内容,也可以通过指针改变这块内存的内容。
#指针操作符&,*
- &:取地址操作符,用于取出一个变量/常量的地址存放到指针变量里(只能接受同类型的变量/常量的地址)。
- *:解引用操作符,用于取出指针变量指向的内存空间里的数据(内容)。
#指针变量的类型对其大小和解引用的影响
- 指针变量的类型:整型指针、字符指针、浮点型指针、数组指针、函数指针、二级指针(存储的是一级指针的地址)、指针数组(数组的每一个元素都是一个指针)等。
- 指针变量的大小:无论何种类型的指针,其大小都只取决于计算机操作系统的位数,在32位环境下,指针的大小都为4个字节,而在64位系统下,指针大小为8个字节。
int* p = 1;
printf("%d ", sizeof(p));//32位环境输出4,64位环境输出8
- 指针类型的意义:指针的操作可以精确到一个字节,而不同的指针类型决定了一次可以操作多少个字节,影响到指针的加减等操作。
举例:1.char*的指针一次向后移动一个字节,而int*一次移动4个字节,*是解引用,b1+1是元素a[1]的地址,b2+1是元素a[4]的地址。
2.指针-指针等于两个地址间的元素个数,若要计算之间的字节数,可以将指针强制转化为char*再相减。
##void型指针
void型指针:可以接收任意类型变量/常量的地址,却不能直接进行整形加减,解引用的操作。但可以用于函数参数的传参,这种应用在文章后半段有涉及。
##const修饰指针变量
const:通常用来修饰变量、数组等,让其在代码运行时不被改变 。也可用来修饰指针变量,有两中效果不同的修饰方式。
- const在 * 左侧:如 int const* a=&a1(效果、用法都等同于const int* a=a1),此时指针变量的内容( a )可以被改变,但指针指向的内容(a1的值)不可以被改变。
2. const在 * 右侧:如int* const b=&b1,此时指针变量的内容( b )不可以被改变,但指针指向的内容( b1 )可以被改变。
总结:指针变量——>指针变量的内容—(@)—>指针指向的内容 , * 在const哪一侧,就可以改变(@)哪一侧。
##野指针
野指针:指向的位置是未知的、随机的、连写代码的本人都不知道的
造成野指针的原因:指针变量未初始化、越界访问、指向的空间释放。
- 指针变量未初始化:未初始化,指针变量的内容将为随机值。
- 越界访问:指针的操作超出数组的边界
int a1[]={1,2,3,4,5};
int* a=a1;
a=&a[0]+100;//此时a指向的地址已不在a1的范围内,a为野指针
- 指向的空间释放:
int add(int a1, int a2){
int n=a1+a2;
return &n;
}
int main(){
int* b=add(6,6);//n为局部变量,在add执行完毕后,为n创建的空间被销毁
//因此赋给b的是一个未知的地址,b为野指针。
}
如何避免野指针:在初始化时可选择初始化为NULL、避免越界访问、不将局部变量的地址赋给指针、使用指针后赋值为NULL同时下次使用前判断是否为NULL。
int* a=NULL;
###assert 断言
assert:(使用会一定程度上增加程序运行时间)用法多样,可用于预防可能出现的程序错误,一般在Debug调试版本里使用,用于作者改进程序,Release是发布版本,供给用户使用,不会用到。使用需包含头文件#include<assert.h>;
assert(expression),若expression为假 ,程序终止(使用assert的一个好处是可以知道错因、在代码的哪一行出错);expression为真,程序继续运行。(下图为assert报错样例)
如何禁用assert:需在头文件 #include<assert.h>前面的代码定义一个宏 #define NDEBUG,如此便可禁用程序里所有的assert断言语句。
##二级指针
二级指针:二级指针存储的是一个指针变量的地址, 二维数组对 & 、* 的用法同一维数组。 可用于动态内存分配、函数参数传递、链表操作、指向指针的数组。
##传值调用
将实参的值传给函数,对形参的操作无法改实参的值,但可以通过指针间接改变实参的值
#数组名是数组首元素的地址
数组名是数组首元素的地址:如 int arr[ ]={1,2,3} , “arr” 便是数组arr的首元素( arr[0] )的地址。
有两个特例:sizeof(arr),&arr里的“arr”表示的是数组的地址。
所以在将一维数组作为参数传参时,实际上传的是一维数组的首元素地址。
因此可以用指针完成对数组的任何操作,需要注意的是 a[i] 等同于 *(a+i),这对于指针后面的二维数组、指针数组等的使用也尤其重要 :例如遍历数组
#字符型指针
字符型指针:字符指针不仅可以指向一个字符,也可以指向一个字符串,如:
char a="sdfdf";
char* a1=&a;
a的地址给了a1,但实际上是将字符串第一个字符‘s’的地址给了a1
#指针数组、数组指针
###二维数组
一维数组在内存中连续存放,二维数组也同样是的,下列例子中二维数组(arr)在第二行首元素地址(arr[1][0])紧跟第一行(arr[0]1])后面。而二维数组的数组名(arr)是第一行的地址,arr[4][2]每一行都相当于一个有2个元素的数组,因此(arr[0])也是(数组arr)的首元素地址,(arr[1])是(数组arr)的第二行地址,以此类推。
而二维数组的数组名(arr)是第一行的地址,arr[4][2]每一行都相当于一个有四个元素的数组,因此(arr[0])也是(数组arr)的首元素地址,(arr[1])是(数组arr)的第二行地址,以此类推。
指针数组:是数组的一种类型,格式是 类型* 变量名 [ ] ,这种数组的每一个元素都是指针,而且指针的类型必须同数组的类型相同,如 int* p[2] ,数组p的两个元素都是整型指针。
指针数组模拟实现二维数组:
int arr1[2] = { 1,2 };
int arr2[2] = { 3,4 };
int* p[2] = { arr1,arr2 };
//p[0]是一个指针,这样初始化相当于 p[0]=arr1;
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
printf("%d ", p[i][j]);
//前面提到*(p+i)等同于p[i]
//因此也可以写为*(p[i]+j)/*(*(p+i)+j);
}
}
数组指针:是指针的一种类型。格式是 类型 (*变量名) [ ] ,形如 int (*p) [2] ,p是一个指向包含2个元素的整型数组的指针,也只能接受这种类型,否则会报错。
引用于二维数组的传参:
#include<stdio.h>
void print1(int (*p1)[2], int n){
for(int i=0; i<2; i++){
for(int j=0; j<n; j++){
printf("%d ",*(*(p+i)+j));
}
}
}
int main(){
int arr[2][2] = { 1,2,3,4};
int(*p)[2] = arr;//赋给数组指针的是二维数组arr第一行的地址,
//第一行相当于一个两个元素的一维数组,因此是合法的
print1(arr,2);
return 0;
}
有一个小知识点:两个不同的字符指针被两个一模一样的字符串赋址, 他们被赋址的地址是同一个地址,这有助于节省空间,而单独创建两个一模一样的字符串,它们的地址却不是同一个。
#函数指针
函数指针:指向一个函数的指针。函数的函数名就是函数的地址,格式是:(返回类型)(*指针变量)(参数1,参数2......)。函数指针只能指向一个返回类型、参数类型相同的函数,如int (*p) (int, int),该指针只能接受一个返回类型为int型且接受两个整型参数的函数(例如:int p1 (int x, int y))。
int (*p) (int)//定义函数指针时(*p)的()不能漏掉
void (*signal(int, void(*) (int) ) (int):这是一个函数声明,同指的是signal函数接受两个参数,一个是int,一个是函数指针(其指向的函数返回类型为void,参数是一个int),返回另一个函数指针(向的是一个void (*) (int)型的函数)。
可以用【(void (*) (int)) signal (int,void (*) (int))】(但是是一个不合法的表达)帮助理解。
#函数指针数组
函数指针数组:数组元素都是同类型的函数的地址,如 int (*p[2]) (int, int), 每一个元素都是一个返回类型为int ,参数为两个int的函数的地址。若将int (函数名)(int)的函数地址存入其中就是不合法的。
#指针的综合应用>>qsort函数
qsor函数:qsort函数是C语言标准库中的一个排序函数,它使用快速排序算法对数组进行排序,需包含头文件<stdlib.h>。在以下代码中,综合应用了指针部分的知识,void*作为能接受任何类型的指针,它是模拟实现qosrt函数的关键,通过回调函数的使用以及对void*型指针的强制类型转换,将其转换整型int*、字符型char*甚至结构体型指针,达到了对任意类型数据的排序。
以下源码有多个模拟,因此有多个main函数,进行测试请分模块单独进行尝试
#include<stdio.h> #include<string.h> #include<stdlib.h> ///冒泡排序 void sort(int arr[], int n){ int i = 0; for (i = 0; i < n - 1; i++){ int j = 0; for (j = 0; j < n - i - 1; j++) { if (arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } } void print(int arr[], int sz) { for (int i = 0; i < sz; i++) printf("%d ", arr[i]); } int main() { int arr[] = { 1,5,7,3,8,6,0,4,9,2 }; int n = sizeof(arr) / sizeof(arr[0]); sort(arr, n); print(arr, n); return 0; } /qosrt函数排序结构体 struct p { char ab[12]; int cd; }; int Cmp_num(const void* p1, const void* p2){//排序字符串结构体成员cd return ((struct p*)p1)->cd - ((struct p*)p2)->cd;//成员选择操作符”.“用于访问对象的成员变量或成员函数, } //而成员访问操作符" -> "用于访问指针所指向的对象的成员变量或成员函数。 int Cmp_char(const void* p1, const void* p2){//排序数字结构体成员ab return strcmp(((struct p*)p1)->ab, ((struct p*)p2)->ab); } int Cmp(void* p1, void* p2){排序数组 return (*(int*)p1 - *(int*)p2); } int main(){ //升序排序 struct p s[] = { {"qweg",12},{"qwee",11} }; int arr[] = { 2,1,4,7,9,8,0,3,5,6 }; qsort(s, sizeof(s) / sizeof(s[0]), sizeof(s[0]), Cmp_char);//排序结构体字符串成员 qsort(s, sizeof(s) / sizeof(s[0]), sizeof(s[0]), Cmp_num);//排序结构体数字成员 qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), Cmp); printf("%s %s %d %d\n", s[0].ab, s[1].ab, s[0].cd, s[1].cd); for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) printf("%d ", arr[i]); return 0; } ///基于冒泡排序算法模拟qsort函数排序结构体; struct p{ char ab[12]; int cd; }; int Cmp_num(const void* p1, const void* p2){ return ((struct p*)p1)->cd - ((struct p*)p2)->cd;//强制类型转换 } int Cmp_char( const void* p1, const void* p2){ return strcmp(((struct p*)p1)->ab, ((struct p*)p2)->ab);//强制类型转换 } void Swap(void* num1, void* num2, int n){ int i = 0; for (i = 0; i < n; i++){ char temp = *((char*)num1 + i); *((char*)num1 + i) = *((char*)num2 + i); *((char*)num2 + i) = temp; } } void bubble_sort(void* base, int count, int types, int (*cMp)(void*, void*)){ int i = 0; for (i = 0; i < count - 1; i++){ int j = 0; for (j = 0; j < count - i - 1; j++) if (cMp((char*)base + j * types, (char*)base + (j + 1) * types) > 0) Swap((char*)base + j * types, (char*)base + (j + 1) * types, types); } } int main(){ struct p s[] = { {"qwex",12},{"qweg",11},{"qweb",15} }; bubble_sort(s, sizeof(s) / sizeof(s[0]), sizeof(s[0]), Cmp_char);//排序结构体字符成员 //bubble_sort(s, sizeof(s) / sizeof(s[0]), sizeof(s[0]), Cmp_num);//重复调用会覆盖前一次的排序! struct p s1[] = { {"qwex",12},{"qweg",11},{"qweb",15} };//为了观察到分别对字符和数字成员的排序,这里拷贝一份结构体 bubble_sort(s1, sizeof(s1) / sizeof(s1[0]), sizeof(s1[0]), Cmp_num);//排序结构体数字成员 printf("%s %s %s ", s[0].ab,s[1].ab, s[2].ab); printf("%d %d %d", s1[0].cd, s1[1].cd, s1[2].cd); return 0; } /基于冒泡排序算法模拟qsort函数排序整形数组 int Cmp(void* p1, void* p2){ return (*(int*)p1 - *(int*)p2);//强制类型转换 } void Swap(void* num1, void* num2, size_t n){ int i = 0; for (i = 0; i < n; i++){ char temp = *((char*)num1 + i);//细化到一个字节一个字节的互换,这样做的好处是可以对任何类型的数组进行数值交换来进行排序 *((char*)num1 + i) = *((char*)num2 + i);//不管是4个字节的int,还是8个字节的double,对于细化到一个字节的char*操作来说都 *((char*)num2 + i) = temp; //绰绰有余。 } } void bubble_sort(void* base, size_t count, size_t types, int (*cMp)(void*, void*)){// count是元素个数,types是一个元素的大小 int i = 0; for (i = 0; i < count - 1; i++){ int j = 0; for(j=0; j<count-i-1; j++) if(cMp((char*)base+j*types,(char*)base+(j+1)*types)>0)// j*types代表第数组arr的j-1个元素 Swap((char*)base + j * types, (char*)base + (j + 1) * types,types); } } void Print(int arr[], size_t sz){ for (int i = 0; i < sz; i++) printf("%d ", arr[i]); } int main(){ int arr[] = { 1,5,7,3,8,6,0,4,9,2 }; bubble_sort(arr, sizeof(arr)/sizeof(arr[0]), sizeof(int), Cmp);//排序整形数组 Print(arr, sizeof(arr) / sizeof(arr[0])); return 0; }
以上便是C语言指针部分的大部分知识,可能忽略了一些细微部分,请谅解。如有错误,请读者指出,我将及时改正!!