char a = 'A';
char* pa = &a;
但是一般来说字符指针很少这么用……更多是拿来存储一个字符串
#include
int main()
{
char str1[] = "hello word.";
char str2[] = "hello word.";
const char *str3 = "hello word.";//将字符串的首字母h的地址存储在str3里面
const char *str4 = "hello word.";
if(str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if(str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
这意味着str3和str4指向的是同一个常量字符串,C/C++会把常量字符串存储到一个单独的内存区域,当几个指针指向同一串字符串时,他们实际是指向同一块内存。但是用相同的常量字符串去初始化不同数组时,数组会对常量字符串进行拷贝,开辟出不同的内存块,所以str1!=str2,但是str3==str4
int main()
{
char* arr[3] = { "abcd", "cdf", "jiuh" };
int i = 0;
for(i = 0; i < 3; i++)
{
printf("%s\n", arr[i]);
}
return 0;
}
比较简单,直接写下code就可以
char* arr[4];//存储了4个char*指针
char** arr[10];//存储了10个char**指针
//复习数组的名字含义
#include
int main()
{
int arr[10] = { 0 };
printf("%p\n", arr);//数组名字就是首元素地址
printf("%p\n", &arr[0]);//取出了首元素地址
printf("%p\n, &arr);//1、取出来整个数组的地址,尽管&arr和&arr[0]的值一样,但是意义不一样(类型不一样),前者是数组类型int[10],后者是整型类型int
printf("%zd\n", sizeof(arr));//2、这里的arr依旧是整个数组
return 0;
}
①sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小
②&数组名,这里取出来的是整个数组地址,尽管它的值和数组的首元素地址相同,两者是有区别的
最大的区别就在于int arr[10]声明后,arr是首元素地址,指针类型是int*,这是个数组指针。而能存放&arr这个地址的指针类型是int(*)[10]。这点在对两种指针进行+/-整数的时候会更加明显,因为指针会根据指向的类型对地址值进行增加 (数组指针后面会讲)
数组指针也是指针
int (*p)[10];//p是一个指向一维整型数组的指针,该数组内含10个int类型
注意[]和()具有相同的优先级,结合性是从左向右结合。并且优先级都比*高。
//第一种使用方法
int main()
{
int arr[10] = {1, 2, 3, 4, 5};
int (*p)[10] = &arr;
return 0;
}语法逻辑没毛病,可以,但是一般不会这么使用
//第二种使用方法
#include
void print_arr1(int arr[3][5], int row, int col)
{
int i = 0;
for(i=0; i<row; i++)
{
for(j=0; j<col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
void print_arr2(int (*arr)[5], int row, int col)
{
int i = 0;
for(i=0; i<row; i++)
{
for(j=0; j<col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = {1,2,3,4,5,6,7,8,9,10};
//数组名arr,表示首元素的地址
//但是二维数组的首元素是二维数组的第一行,指向这一行的指针类型可以写成int [][5]或者int (*)[5]
//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
print_arr1(arr, 3, 5);
print_arr2(arr, 3, 5);
return 0;
}
int arr[5];//单纯是个整型数组
int* parr1[10];//单纯是一个指针数组,每一个指针都指向一个int
int (*parr2)[10];//数组指针,该指针指向一个包含10个int元素的数组
int (*parr3[10])[5];//指针数组,可以思考成“int(*)[5] parr3[10]”(与int arr[5]是类似的)很明显,这是一个包含10个元素的数组,每个元素都int(*)[5]这种类型的指针,这种指针指向一包含5个元素的数组
#include
void test1(int arr[])//可以这么写
{
//code
}
void test1(int arr[10])//10写与不写都行,无所谓,C会将它忽略
{
//code
}
void test1(int *arr)//可以的,传过来的arr1的拷贝是一个int元素地址
{
//code
}
void test2(int *arr[20])//20写与不写都行,C依旧会将它忽略
{
//code
}
void test2(int **arr)//也可以,传过来的arr2的拷贝是一个int*元素的地址
{
//code
}
int main()
{
int arr[10] = {0};
int *arr2[20] = {0};
test(arr1);
test2(arr2);
}
void test(int arr[3][5])//可以使用,不过3会被忽略
{
//code
}
void test(int arr[][])//不可以使用,5必须留下来
{
//code
}
void test(int arr[][5])//可以使用
{
//code
}
//二维数组传参,函数形参的设计只能省略第一个[]的数字。
//对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
void test(int *arr)//不行,类型不匹配,拷贝过来的参数是一个指向一维数组的指针,而这个参数仅仅是一个一维指针
{
//code
}
void test(int* arr[5])//不行,这是一个指针数组,根本没有关系
{
//code
}
void test(int (*arr)[5])//可以这么写,指针类型匹配了
{
//code
}
void test(int **arr)//不行,指针类型不匹配
{
//code
}
int main()
{
int arr[3][5] = {0};
test(arr);
}
//使用二维数组指针(深刻理解)
void print(int(*p)[20], int x, int y)
{
//二维数组的名字就是首元素的地址,其地址就是第一行数组的地址,也就是指向一维数组类型的指针,因此不能写形参为int**
//又因为一维数组的整体地址&arr和其首元素地址&arr[0]起始位置相同,故从值来看是一样的,但是两者的指针类型完全不同
for (int i = 0; i < x; i++)
{
for (int j = 0; j < y; j++)
{
printf("%d", *(* (p + i) + j));//得到数组名,利用i得到每一行的数组名,利用j得到某一行的每一列的元素地址
}
}
}
int main()
{
int arr[10][20] = { {1, 2, 3}, {2, 3, 4} };
print(arr, 10, 20);
return 0;
}
#include
void print(int *p, int sz)
{
int i = 0;
for(i=0; i<sz; i++)
{
printf("%d\n", *(p+i));
}
}
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9};
int *p = arr;
int sz = sizeof(arr)/sizeof(arr[0]);
//一级指针p,传给函数
print(p, sz);
return 0;
}
#include
void test(int** ptr)
{
printf("num = %d\n", **ptr);
}
int main()
{
int n = 10;
int*p = &n;
int **pp = &p;
test(pp);
test(&p);
return 0;
}
指针的类型一定要匹配好,尽管类型不匹配的时候依旧可以进行传参(因为所有地址在同一个平台都是一样大小的,只是存储一个地址,理论上用什么类型的指针都可以存储),但是在后续使用指针的时候(解引用指针)就会发生错误,指针会根据指针类型访问不同大小的内存
对于函数function(),其函数指针类型为【返回值 (*指针名) (参数类型的列表)】
若想使用这个函数就要进行解引用,使用【(* pf)(参数列表)】或者【pf(参数列表)】都可以。编译器在处理的时候,没有 * 也行,但是要用 * 就一定要加括号
//例子
char* test(int c, float* pf)
{
//某些代码
}
int main()
{
char* (*pt)(int, float*)pf = test;
test(参数列表)
return 0;
}
//代码1
(*( void (*)() )0)();
//从最里面开始理解void(*)()是一个指向“返回值为空,参数列表为空”函数的函数指针
//然后将0的int类型强制转化为函数指针类型,于是0成了一个函数指针类型的地址
//再解引用0这个地址,得到0地址处的函数,然后使用这个函数
//代码2
void (* signal(int , void(*)(int)) )(int);
//等价代码如下
//typedef void(*pfun_t)(int);//注意pfun_t是一个和void(*)(int)同类型名,将pfun_t放在*旁边是为了指明pfun_t是一个指针而已,这只是语法形式要求
//pfun_t signal(int, pfun_t);
//因此有一个简化代码的技巧就是使用typedef
这两段代码来自于《C陷阱与缺陷》,是本出名的C语言书籍
#include
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a*b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
do
{
printf("*************************\n");
printf(" 1:add 2:sub \n");
printf(" 3:mul 4:div \n");
printf("*************************\n");
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = add(x, y);
printf("ret = %d\n", ret);
break;
case 2:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = sub(x, y);
printf("ret = %d\n", ret);
break;
case 3:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = mul(x, y);
printf("ret = %d\n", ret);
break;
case 4:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = div(x, y);
printf("ret = %d\n", ret);
break;
case 0:
printf("退出程序\n");
breark;
default:
printf("选择错误\n");
break;
}
}
while (input);
return 0
}
#include
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a*b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表,即函数指针数组,这里的0(或者写NULL)起到占位的作用,理论上放什么都行,只要后续处理好就行…
while (input)
{
printf("*************************\n");
printf(" 1:add 2:sub \n");
printf(" 3:mul 4:div \n");
printf("*************************\n");
printf("请选择:");
scanf("%d", &input);
if ((input <= 4) && (input >= 1))
{
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = (*p[input])(x, y);
}
else
{
printf("输入有误\n");
}
printf("ret = %d\n", ret);
}
return 0;
}
//当计算器后续需要加入更多的运算函数时,那么使用开关语句就会显得冗长,但是使用函数指针数组就不会有这个问题,这将会大大缩减代码。但是使用函数指针也具有有缺点,它只能存放同样函数签名(函数签名定义了函数的输入和输出,即:函数签名==参数+返回值)的函数
像类似上面使用函数指针数组的方法就叫做转移表,具有一种跳转使用函数的效果。即“函数指针数组”==“转移表”
void test(const char* str)
{
printf("%s\n", str);
}
int main()
{
//声明函数指针pfun,并且进行初始化
void (*pfun)(const char*) = test;
//声明一个函数指针的数组pfunArr
void (*pfunArr[5])(const char* str) = { pfun };
pfunArr[0] = test;
//指向函数指针数组pfunArr的指针ppfunArr
void (*(*ppfunArr)[5])(const char*) = &pfunArr;
return 0;
}
//*代表ppfunArr是指针,[]代表这个指针指向一个内含5元素的数组,而每个元素的类型都是void (*)(const char*)
//因此按照运算符的顺序来解读是比较快的
void qsort(
void *base,//指向了待排序数组的第一个元素
size_t nitems,//排序的元素个数
size_t size,//每个元素的大小,单位是字节
int (*compar)(const void*, const void*)//指向一个函数,这个函数可以比较两个元素的大小
);
//其底层是使用快速排序的方法来排序的,依靠compar指向的不同函数内部不同的比较,可以解决不同类型的数据快速排序
#include
#include
//qosrt函数的使用者得实现一个比较函数
int int_cmp(const void * p1, const void * p2)
{
return ( *(int*)p1 - *(int*)p2 );//注意不能直接解引用void指针,另外如果倒过来就是逆序输出了
}
int main()
{
int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
int i = 0;
qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
{
printf( "%d ", arr[i]);
}
printf("\n");
return 0;
}
#include
int int_cmp(const void * p1, const void * p2)//其中一个比较函数,这个是整型比较,是用户决定这个函数应该如何编写
{
return (*(int*)p1 - *(int*)p2);
//不过注意void不能直接解引用!!!
}
void _swap(void *p1, void * p2, int size)//其中一个排序方法,这个是排序是冒泡排序,可以由开发者决定底层排序的方法,在qsort中使用的底层函数是快排
{
int i = 0;
for (i = 0; i< size; i++)//之所以这么做,是因为没有办法预测有多少个字节,只能通过一个一个字节进行交换,最后所有字节进行交换,即两个数据进行了交换
{
char tmp = *((char*)p1 + i);
*((char*)p1 + i) = *((char*)p2 + i);
*((char*)p2 + i) = tmp;
}
}
void bubble(void *base, int count, int size, int(*cmp )(void*, void*))//相当于qsort,但是实现逻辑不仅仅是快速排序,内部的_swap函数也可能是其他的排序算法,cmp函数可能比较不同类型的数据。 注意base是void*类型,写int*会写死的,只能限定于整型
{
int i = 0;
int j = 0;
for (i = 0; i < count - 1; i++)
{
int flag = 1;//①优化代码,若是没有交换就说明不需要经过排序就是有序的了
for (j = 0; j < count - i - 1; j++)
{
if (cmp ((char*) base + j * size, (char*)base + (j + 1) * size) > 0)//比较函数,这里改成(char*)就可以利用size适应不同的字节
{
flag = 0;
_swap((char*)base + j * size, (char*)base + (j + 1) * size, size);//排序函数,这里改成(char*)就可以利用size适应不同的字节
}
if(flag == 1)
{
break;
}
}
}
}
int main()
{
int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
//char *arr[] = {"aaaa","dddd","cccc","bbbb"};
int i = 0;
bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
{
printf( "%d ", arr[i]);
}
printf("\n");
return 0;
}
注意我们不是在模拟qsort函数,而是作一个类似qsort函数的“冒泡排序通用的bubble函数”
指针和数组不是同一个东西,非等价,但是二者关系密切
//①sizeof(数组名)
//②&数组名
//以上两种情况数组名都是整个数组,注意这里的数组名必须是单独放进去一个数组名!
对于这两种情况,你还需要一些深刻的理解
//一维数组
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));//16
printf("%d\n",sizeof(a+0));//4或者8,注意这里的数组名a受到+0的影响,a是首元素地址
printf("%d\n",sizeof(*a));//4,注意这里的数组名a受到*的影响,a是首元素地址,(*a)则找到了一个首元素
printf("%d\n",sizeof(a+1));//4或者8,注意这里的数组名a受到+1的影响,a是首元素地址,(a+1)是数组的第二个元素
printf("%d\n",sizeof(a[1]));//4,就是第二个元素
printf("%d\n",sizeof(&a));//这里取出来的地址是数组的整个地址,但是也是地址,因此是4或者8。不要理解为二级指针!!!
printf("%d\n",sizeof(*&a));//16,(*(&a))就是取出整个数组
printf("%d\n",sizeof(&a+1));//4或者8,&a取出整个数组地址,+1后跳过一个指针
printf("%d\n",sizeof(&a[0]));//4或者8,首元素地址,依旧是地址
printf("%d\n",sizeof(&a[0]+1));//4或者8,第二个元素的地址,依旧是地址
//------------------------------------------------------------
//字符数组
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));//6,arr是整个数组
printf("%d\n", sizeof(arr+0));//4或者8,arr受到+0的影响,是首元素地址,还是地址
printf("%d\n", sizeof(*arr));//1,这里arr受到*的影响,是数组首元素地址,*arr解引用首元素地址得到首元素
printf("%d\n", sizeof(arr[1]));//1,arr[1]是数组第二个元素
printf("%d\n", sizeof(&arr));//4或者8,&arr是取出了整个数组的地址,还是地址
printf("%d\n", sizeof(&arr+1));//4或者8,&arr取出了整个数组,+1跳过了以个数组,得到一个地址,依旧是地址
printf("%d\n", sizeof(&arr[0]+1));//4或者8,&arr[0]取出首元素的地址,+1后得到第二个元素的地址,还是地址
printf("%d\n", strlen(arr));//随机值,因为没有\0
printf("%d\n", strlen(arr+0));//随机值,因为没有\0
printf("%d\n", strlen(*arr));//传参错误,strlen传进去应该是地址,而这里传进去了一个字符(*arr),出现error,非法访问(越界访问)。而且在函数strlen中会解引用传进来的地址,因此解引用了'c',即:解引用97,在VS2022会发生“已引发异常”的警告。
printf("%d\n", strlen(arr[1]));//传参错误,strlen传进去应该是地址,而这里传进去了一个字符(arr[1]),出现error,非法访问(越界访问),在VS2022会发生“已引发异常”的警告
printf("%d\n", strlen(&arr));//随机值,因为没有\0。注意和上一条的随机值是一样的,都要访问后面的字符
printf("%d\n", strlen(&arr+1));//随机值,因为没有\0。注意和上一条的随机值是不太一样的,一定比上一条长度差6
printf("%d\n", strlen(&arr[0]+1));//随机值,因为没有\0。注意比strlen(&arr)的随机值差了个1
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));//7,arr就是整个数组的地址,注意'\0'也要算进去
printf("%d\n", sizeof(arr+0));//4或者8,arr受到+0的影响,是首元素地址的指针,arr+0则是首元素地址,还是地址
printf("%d\n", sizeof(*arr));//1,受到*影响,arr是首元素地址,*arr解引用首元素地址得到首元素。可以利用这一点写sizeof(arr)/sizeof(arr[0])为sizeof(arr)/sizeof(*arr)
printf("%d\n", sizeof(arr[1]));//1,arr[1]就是第二个元素
printf("%d\n", sizeof(&arr));//4或者8,&arr取出了整个数组的地址,还是地址
printf("%d\n", sizeof(&arr+1));//4或者8,&arr取出整个地址,+1跳过了一个大小为7的数组地址,但是还是地址
printf("%d\n", sizeof(&arr[0]+1));//4或者8,&arr[0]+1得到第二个元素的地址,还是地址
printf("%d\n", strlen(arr));//6,计算出字符串长度
printf("%d\n", strlen(arr+0));//6,arr受到+0的影响,arr是一个首元素地址,+0还是首元素地址,strlen的参数就是一个地址,解引用后,就从首元素一直往后面找'\0',找到就停下,因此还是6
printf("%d\n", strlen(*arr));//传参错误,strlen传进去应该是地址,而这里传进去了一个字符(*arr),出现error,非法访问(越界访问)。
printf("%d\n", strlen(arr[1]));//传参错误,strlen传进去应该是地址,而这里传进去了一个字符(*arr),出现error,非法访问(越界访问)。
printf("%d\n", strlen(&arr));//6,&arr取出整个数组的地址,虽然指针的类型是指向整个数组,但是由于形参的类型是char*,它把传过来的指针类型自动强制类型转换了(所以这里如果是vs2022,这里会出现一个类型不匹配的警告,但是不影响使用),而&arr的值和arr数组的首元素地址值相同,故在strlen函数内部,指针++的时候,依旧是按照一个又一个字符地址的顺序解引用,直到找到'\0'就停止,因此求出来还是6
/*这个头文件中strlen的声明
size_t __cdecl strlen(
_In_z_ char const* _Str
);
*/
printf("%d\n", strlen(&arr+1));//随机值,和上面的代码有点类似,但是由于取出整个数组的地址后,+1跳过了一个数组地址,后面没有固定的'\0'
printf("%d\n", strlen(&arr[0]+1));//6,&arr[0]得到首元素地址,+1得到第二个元素的地址,因此从第二个元素地址开始向后解引用,直到找到'\0'为止
char *p = "abcdef";
printf("%d\n", sizeof(p));//4或者8,传入了一个char*类型的地址,还是一个地址
printf("%d\n", sizeof(p+1));//4或者6,p是首元素地址,p+1就是第二个元素地址,还是地址
printf("%d\n", sizeof(*p));//1,*p得到的是一个元素
printf("%d\n", sizeof(p[0]));//1,p[0]还是第一个元素
printf("%d\n", sizeof(&p));//4或者8,&p得到指针的指针
printf("%d\n", sizeof(&p+1));//4或者8,指针的指针+1,还是地址。注意p是char*类型,指向一个char;&p是char**类型,指向一个char*。因此跳过一个p的地址
printf("%d\n", sizeof(&p[0]+1));//4或者8,&p[0]取出首元素地址,&p[0]+1得到第二个元素的地址,还是地址
printf("%d\n", strlen(p));//6,p是字符串首元素的地址,从第一个元素地址开始解引用,直到找到'\0'
printf("%d\n", strlen(p+1));//5,p+1是第二个元素的地址,从第二个元素地址开始解引用,直到找到'\0'
printf("%d\n", strlen(*p));//传参错误,解引用得到字符,越界访问
printf("%d\n", strlen(p[0]));//传参错误,解引用得到字符,越界访问
printf("%d\n", strlen(&p));//随机值,p是char*类型指针,&p是char**类型指针,被强制转化为char*类型的指针,但是没办法确定&p的地址后面是否有'\0'的地址,故随机值
printf("%d\n", strlen(&p+1));//随机值,产生的随机值和上面的随机值没有关系(因为已经&p+1已经跳过一个p了,如果p的后面就有'\0'地址,&p+1这个地址可能比'\0'的地址还靠后)
printf("%d\n", strlen(&p[0]+1));//5,&p[0]是首元素地址,+1就是下一个元素地址,从第二个元素的地址开始解引用,直到找到'\0'为止
//------------------------------------------------------------
//二维数组
//这一部分要深刻理解好一个概念:例如定义了int a[3][4]后,则a[0][1]是指一个名字为a[0]的数组的第二个元素,a[0]+1就是第一行数组的第二个元素的地址
int a[3][4] = {0};
printf("%d\n",sizeof(a));//48,注意这里的a是整个数组!!!因此计算每个数组的大小
printf("%d\n",sizeof(a[0][0]));//4,a[0][0]就是一个数组元素
printf("%d\n",sizeof(a[0]));//16,a[0]是第一行数组的数组名字,即整个数组的地址,内含有4个元素,计算的是整个行数组的大小
printf("%d\n",sizeof(a[0]+1));//4或者8,a[0]原本是一个第一行数组的名字,受到+1影响,这里的a[0]不再是整个行数组地址,而是该行首元素地址a[0]+1得到这一行的第二个元素地址,还是地址
printf("%d\n",sizeof(*(a[0]+1)));//4,(a[0]+1)是第一行数组的第二个元素地址,对这个地址解引用得到a[0][1]这个元素,这个元素类型是int
printf("%d\n",sizeof(a+1));//4或者8,a受到+1影响,a代表二维数组的首元素地址,即指向第一行的地址,+1后得到第二行的地址,还是地址
printf("%d\n",sizeof(*(a+1)));//16,a受到+1的影响,a代表第一行的地址,a+1后得到第二行的地址,对这个地址解引用得到整个第二行,类型是int[4],计算出整个行数组的大小
printf("%d\n",sizeof(&a[0]+1));//4或者8,&a[0]是整个第一行数组的地址,+1得到整个第二行数组的地址,依旧是地址
printf("%d\n",sizeof(*(&a[0]+1)));//16,&a[0]是整个第一行地址,+1得到整个第二行地址,对这个地址解引用,得到整个第二行数组,类型为int[4],因此计算整个第二行数组大小
printf("%d\n",sizeof(*a));//16,受到*影响,a是首元素地址,解引用得到整个第一行数组,类型为int[4],故计算整个数组的大小
printf("%d\n",sizeof(a[3]));//16,a[3]是第四行数组的数组名字,故计算整个第四行数组的大小。但是注意sizeof只通过类型计算大小,不需要进行运算,并且在编译过后就直接替换了原本的整个sizeof表达式,因此这里不属于越界访问