目录
5.2.3 当指针变量不再使用时,及时将变量值为NULL,指针使用之前检查有效性
1.指针
1.1 指针是什么?
指针就是一个变量,用来保存地址的
我们可以这样理解:内存单元的编号 = 地址 = 指针
32位机器上有32根地址总线,能表示2^32种不同的含义,每种含义都代表着一个地址。
1.2 指针变量和地址
1.2.1 & - 取地址操作符
我们在C语言中创建变量其实就是在向内存申请空间,比如:
int main()
{
int p = 6;
return 0;
}
上述代码表示创建了整型变量a,向内存中申请4个字节,用于存放整型数据,并且每个字节都有地址。
那我们如何得到一个变量的地址呢?
使用 & -- 取地址操作符
int main()
{
int p = 6;
printf("%p\n",&p);//&p表示取出p的地址
return 0;
}
这里&p取出的地址,是p占用的4个字节中最小字节的地址。
如下图所示:
这里的整型变量虽说占用了4个字节,只要知道第一个字节地址,就能计算出其他3个字节的地址。
1.3 * - 解引用操作符
1.3.1 指针变量
通过 & - 取地址操作符得到的地址是一个数值,有时候我们需要用到这个数值,在后续使用的时候可以直接用,所以要先存储起来,这样的地址值我们存放在指针变量中。
例如:
int x = 6;
int* p = &x;//取出x的地址,存储在指针变量p中
注意:指针变量是一种变量,这种变量是用来存放地址的,一般存放在指针变量中的值都会理解为地址。
1.3.2 拆解指针类型
看这段代码,这里p的类型是 int*,怎样理解指针的类型?
int x = 6;
int* p = &x;
在p的左侧 类型是int*, * 在说明 p 是一个指针变量,而int是在说明 p 指向的是整型(int)类型的对象。
若有一个char类型的变量c,c的地址便存储在char*类型的指针变量中,如下:
char c = 'h';
char* pc = &c;
1.3.3 解引用操作符 - *
在C语言中,我们用指针变量存储的地址,可以使用解引用操作符找到指向的对象,拿到具体的数据。
#include <stdio.h>
int main()
{
int x = 6;
int* p = &x;
printf("p = %d\n",*p):
return 0;
}
在上述代码中,我们使用到了解引用操作符 - * ,*p 表示用p存放的地址,找到指向的空间,这里*p和x指向的是同一块空间,所以上述代码可以打印*p 的值为 6;
1.4 指针变量的大小
C语言中,指针变量的大小取决于地址的大小
在32位的平台下,地址是32个bit,那么指针变量的大小就是4;
在64位的平台下,地址是64个bit,那么指针变量的大小就是8。
注意:
指针变量的大小和类型无关,只要是指针类型的变量,在相同的平台下,大小都是相同的。
2.指针变量的类型有什么意义
通过上面的学习,我们知道了指针变量的大小在相同平台下,大小都是一样的,那么为什么还要分出各种类型呢?通过下面的学习,这个疑惑你就知道:
2.1 指针的解引用
测试下面两段代码,观察在调试时内存的变化。
//测试代码1
#include <stdio.h>
int main()
{
int x = "0x33445566";
int* px = &x;
*px = 0;
return 0;
}
//测试代码2
#include <stdio.h>
int main()
{
int x = "0x33445566";
char* pc = (char*)&x;
*pc = 0;
return 0;
}
调试代码1结果:
调试代码2的结果:
观察调试结果可以看到,代码1会将x的4个字节全部改成0,但代码2只会将x第一个字节修改为0.
结论:指针的类型决定了,对指针解引用时一次能操作几个字节。例如:char*类型的指针解引用指针访问一个字节,但int*类型的指针解引用能访问4个字节。
2.2 指针 +- 整数
调试下面一段代码,观察地址在内存中的变化:
#include <stdio.h>
int main()
{
int x = 6;
int* px = (char*)&x;
char* pc = (char*)&x;
printf("&x = %p\n", &x);
printf("&px = %p\n", px);
printf("&px+1 = %p\n", px + 1);
printf("&pc = %p\n", pc);
printf("&pc+1 = %p\n", pc + 1);
return 0;
}
代码运行结果如下图:
可以看到,int* 类型的指针变量+1,跳过了4个字节,而char* 类型的指针变量+1,只跳过了一个字节。这就是指针变量类型不同所产生的差异。指针+1,是指跳过指针指向的一个元素。指针可以+1,指针也可以-1。
结论:指针的类型决定了指针向前或向后跳过几个字节。
2.3 void* 指针
C语言中有一种特殊的指针类型 ——void* 类型,可以理解为无具体类型的指针(或泛型指针),这种指针可以接收任意类型的地址。但是它有局限性,void* 的指针不能直接+-整数和进行解引用的运算。
观察:
#include <stdio.h>
int main()
{
int x = 6;
int* px = &x;
char* pc = &x;
return 0;
}
上面的代码中,我们将一个int类型的变量地址赋值给一个char*类型的指针变量。编译器给了一个警告——类型不兼容。但使用void* 类型不会出现这种情况。
使用void* 类型指针接收地址:
#include <stdio.h>
int main()
{
int x = 6;
void* px = &x;
void* pc = &x;
*px = 55;
*pc = 0;
return 0;
}
代码编译结果:
出现这种情况的原因是:使用void*类型的指针可以接收任何类型的地址,但是无法直接进行指针的运算。
一般 void* 类型的指针是用在函数参数的部分,可以用来接收不同数据类型的地址,使用这种方式编程可以大大缩减代码量(泛型编程),使用一个函数处理多种数据类型,在下面知识中会讲解到。
3. const 修饰指针
3.1 const 修饰变量
变量的值是可以修改的,把一个变量的地址存放到指针变量中,通过指针变量也可以修改这个变量。如果我们希望一个变量不被修改,就需要给它加上一些限制,这接下来要讲解的 const 的作用。
#include <stdio.h>
int main()
{
int a = 10;
a = 100;//a的值修改为100
const int b = 0;
b = 20;//b的值不能修改
return 0;
}
上面的代码中,a的本质是一个变量,是可以被修改的,但是b被const修饰了,是一个常量,不能被修改,在编译器中我们对b进行修改,不符合语法规则,编译器会报错。
但是还是有方法去修改b的值,我们可以绕过b,直接使用b的地址,修改b就可以实现,虽然这种做法是在打破语法规则。
#include <stdio.h>
int main()
{
const int a = 0;
printf("a = %d\n", a);
int* pa = &a;
*pa = 100;
printf("a = %d\n", *pa);
return 0;
}
代码运行结果:
观察运行结果,a 确实被指针变量p修改了,可是我们使用const修饰a就是为了不让a被修改,但是指针变量p拿到a的地址就可以修改a,这样就破解了const的限制,显然这是不合理的,我们要做到p拿到a的地址也不能修改a,就得使用const修饰指针变量。
3.2 const修饰指针变量
const修饰指针变量,const可以放在 * 的左侧,也可以放在 * 的右侧,但意义是不一样的。
int p ;//没有被const修饰
const int * p;//const在 * 的左侧
int * const p;//const在 * 的右侧
分析下面一段代码:
#include <stdio.h>
//测试1 - 无const修饰的
void test1()
{
int* a = 10;
a = 20;//ok
*a = 30;//ok
}
//测试2 - const在*左侧
void test2()
{
const int* a = 10;
int b = 20;
*a = &b;//err
a = 30;//ok
}
//测试3 - const在*右侧
void test3()
{
int* const a = 10;
int b = 20;
*a = &b;//ok
a = 30;//err
}
//测试4 - cosnt在*两侧
void test4()
{
const int* const a = 10;
int b = 20;
*a = &b;//err
a = 30;//err
}
int main()
{
//测试代码1 - 无const修饰
test1();
//测试代码2 - const在 * 左侧
test2();
//测试代码3 - const在 * 右侧
test3();
//测试代码4 - const在 * 两侧
test4();
return 0;
}
通过上述代码测试,我们可以得出结论:
结论:const修饰指针变量时,
const在*左侧,修饰对象是指针指向的内容,确保指针指针的内容不能通过指针修改。
const在*右侧,修饰对象是指针变量本身,确保指针变量的内容不能修改,但是指针指向的内容,是可以通过指针修改。
4. 指针的运算
指针的基本运算有三种,分别是
- 指针 +- 整数
- 指针 - 指针
- 指针的关系运算
4.1 指针 +- 整数的运算
数组在内存中的存储地址是连续的,只要知道第一个元素的地址,就可以计算后面所有元素的地址。
#include <stdio.h>
//指针 +- 整数的 运算
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int len = sizeof(arr) / sizeof(arr[0]);
int* p = arr;
int i = 0;
for (i = 0; i < len; i++)
{
printf("%d ", *(p + i));//(p+i)就是指针+整数的形式
}
return 0;
}
代码运行结果:
4.2 指针 - 指针的运算
#include <stdio.h>
int MyStrlen(char* str)
{
char* p = str;
while (*p != '\0')
{
p++;
}
return p - str;
}
//指针 - 指针
int main()
{
char str[] = "abcdef";
int ret = MyStrlen(str);
printf("ret = %d\n", ret);
return 0;
}
代码运行结果:
4.3 指针的关系运算
#include <stdio.h>
//指针的关系运算
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
int sz = sizeof(arr) / sizeof(arr[0]);
while (p < arr + sz)
{
printf("%d ", *p);
p++;
}
return 0;
}
5.野指针介绍
野指针的概念:野指针就是指针指向的位置是未知的(随机地、不正确的、没有明确限制的)。
5.1 野指针的产生
1.指针没有初始化
例如:
#include <stdio.h>
int main()
{
int* a;
*a = 20;//err
return 0;
}
编译器报错:
2.指针越界访问
#include <stdio.h>
int main()
{
int arr[6] = {0,};
int* p = &arr[0];
int i = 0;
for (i = 0; i <= 8; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*p = i;
p++;
}
return 0;
}
3.指针指向的空间释放
#include <stdio.h>
int* test()
{
int n = 20;//局部变量,作用域仅在函数内部
return &n;
}
int main()
{
int* r = test();//test结束后,*r就变成野指针
printf("%d\n", *r);//这里的 *r 已经是野指针了
return 0;
}
5.2 规避野指针的方法
5.2.1 指针初始化
在明确知道指针的指向对象就直接赋值(地址值),如果不知道明确指向,可以先赋值 NULL ,NULL是C语言中定义的一个标识符常量,它的值为0,0也是一个地址,但是这个地址是无法使用的,读写该地址,编译器会报错。
初始化的定义如下:
#include <stdio.h>
int main()
{
int n = 6;
int* p = &n;//方式一
int* q = NULL;//方式二
return 0;
}
5.2.2 留意指针是否越界
一个程序向内存申请多少空间,通过指针就能访问多少空间,但绝不能超出范围访问,否则就是越界访问。
5.2.3 当指针变量不再使用时,及时将变量值为NULL,指针使用之前检查有效性
前期用过的指针变量,后期不再使用时,及时置为NULL。有个规则是:只要指针指向的是NULL,就不再访问,同时在使用指针之前,可以判断指针是否为NULL。
用法如下:
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int* p = &arr[0];
int i = 0;
for (i = 0; i < 10; i++)
{
*p = i;
p++;
}
//代码执行到这里,p已越界了,此时可以把p置为NULL;
p = NULL;
p = &arr[0];//让p重新获取地址
printf("%d ", p);
if (p != NULL)
{
//…
}
return 0;
}
5.2.4 避免返回局部变量的地址
6.assert 断言
宏assert()的头文件是<assert.h>,使用场景:在运行时确保程序符合指定的条件,如果不符合,就终止运行。这个宏常被称为”断言“。
assert(p != NULL);
上面这行代码,就可以用来在使用指针时,判断指针指向的是否时NULL,如果不等于NULL,则可以继续执行后面的代码,否则就会终止运行,并给出报错信息提示。
assert() 宏接收一个表达式作为参数。如果表达式为真(返回值非0),assert()就不产生作用,程序会继续执行。如果表达式为假(返回值为0),assert()就会报错。
assert()的使用让我们编写程序更加方便,使用assert()有几个好处:它不仅能自动识别文件和出问题的行数,还有一种无需更改代码就能开启或关闭assert()的机制。如果确定程序没有问题,无需再做断言,就可以在 #include <assert.h>语句前,定义一个宏NDEBG。
#define NDEBUG
#include <assert.h>
然后,再重新编译程序,编译器会禁用文件中所有的assert()语句。后续程序又出现问题,可以移除这条#defind NDEBUG,指令(也可以把它注释了),再次编译,就可以重新启用assert()语句。
使用assert()的缺点是:引入额外的检查,增加了程序的运行时间。
一般我们在Debug模式下使用,在Release版本中选择禁用asserty()语句就可以。在VS这个集成开发环境中,Release版本下,直接是优化掉了。
7.指针的使用和传址调用
7.1 模拟strlen的实现
库函数strlen的功能是求字符串的长度,统计在'\0'之前出现的字符个数。
函数原型:
size_t strlen(const char* str);
计算方式为:参数str接字符串第一个字符位置,然后开始统计字符串中 '\0' 之前的字符个数,最后返回长度。
思考一下:如果我们要实现这个功能,就要从字符串的起始位置向后遍历,不是'\0',计数器就+1,遇到'\0'就停止。
#include <stdio.h>
#include <assert.h>
size_t MyStrlen(const char* s)
{
int count = 0;
assert(s);
while (*s != '\0')
{
count++;
s++;
}
return count;
}
int main()
{
char str[] = "abcdef";
int len = MyStrlen(str);
printf("len = %d\n", len);//输出结果:len = 6
return 0;
}
7.2 传值调用和传址调用的区别
题目:写一个函数,交换两个整型变量的值
//写一个函数,交换两个整型变量的值
#include <stdio.h>
void SwapNum(int a, int b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int n = 2;
int m = 4;
printf("交换前:n = %d,m = %d\n", n, m);
SwapNum(n, m);
printf("交换后:n = %d,m = %d\n", n, m);
return 0;
}
代码运行结果:
程序执行完并没有产生交换的效果是什么原因?
通过调试,我们知道在main函数内部创建的变量n、m与函数中接收的a、b的地址并不是同一个,当我们调用SwapNum函数时,将n和m的值传递给了SwapNum函数,SwapNum函数内创建了形参a和b接收n与m的值。这里a与b确实接收了n和m的值,但a和b的地址与n和m的地址无关,故在函数中交换a与b的值不会影响n和m,当函数结束调用后回到main函数中,a和b的值并没有发生变化,SwapNum函数这种把变量本身传递过去的方式,叫做传值调用。
结论:实参传值给形参,形参会单独创建一个临时变量接收实参,对形参的修改不会影响到实参。
如果我们实现当调用SwapNum函数时,SwapNum函数内部操作的地址和main函数n和m是 同一块空间,就可以使用指针。在main函数中直接将n和m的地址传给SwapNum函数,函数就可以间接操作main函数中的n和m,实现交换效果。
#include <stdio.h>
void SwapNum(int* a, int* b)
{
char tmp = *a;
*a = *b;
*b = tmp;
}
int main()
{
int n = 2;
int m = 4;
printf("交换前:n = %d,m = %d\n", n, m);
SwapNum(&n, &m);
printf("交换后:n = %d,m = %d\n", n, m);
return 0;
}
代码运行结果:
这种把变量的地址传给函数的方式叫做传址调用。
结论:传址调用,可以让函数和主函数之间的地址建立真正的联系,在函数内部可修改主调函数中的变量。在以后的函数中只是需要主调函数中的变量值计算,就可使用传值调用;如果函数内部要修改主调函数中变量的值,就使用传址调用的方式。
8.数组名理解
8.1 数组名的理解
先看一段代码:
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
printf("&arr[0 ] = %p\n", &arr);
printf("arr = %p\n", arr);
return 0;
}
运行结果:
这里我们发现数组名和数组首元素的地址是同一个,数组名就是数组首元素的地址。但有两个例外 :
- sizeof(数组名):sizeof 中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节(Byte).
- &数组名:这里的数组名表示整个数组,取地址取出的是整个数组的地址(注意:整个数组的地址和数组首元素的地址是有区别的)
除此之外,任何地方使用数组名,数组名都表示首元素的地址。
通过下面这段代码可观察出区别:
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr = %p\n", &arr);
printf("&arr + 1 = %p\n", &arr + 1);
printf("arr = %p\n", arr);
printf("arr + 1 = %p\n", arr + 1);
printf("&arr[0] = %p\n", &arr[0]);
printf("&arr[0] + 1 = %p\n", &arr[0] + 1);
return 0;
}
代码运行结果:
arr和arr+1相差4个字节,&arr[0]和&arr[0]+1相差4个字节, 原因在于arr和&arr[0]都指向首元素的地址,+1就表示跳过一个元素。但&arr与&arr+1相差40个字节,是因为 &arr 表示数组的地址,+1就跳过整个数组。这就是arr和arr[0]以及&arr三者的区别。
9.使用指针访问数组
有前面知识的铺垫,结合数组的特点,我们可以方便的使用指针访问数组。
#include <stdio.h>
//方式一
void print1(int arr[], int sz)
{
int i = 0;
int* p = arr;
for (i = 0; i < sz; i++)
{
printf("%d ", *(p+i));
}
}
//方式二
void print2(int arr[], int sz)
{
int i = 0;
int* p = arr;
for (i = 0; i < sz; i++)
{
printf("%d ", p[i]);
}
}
//方式三
void print3(int arr[], int sz)
{
int i = 0;
int* p = arr;
for (i = 0; i < sz; i++)
{
printf("%d ", *p);
p++;
}
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int len = sizeof(arr) / sizeof(arr[0]);
print1(arr, len);
printf("\n");
print2(arr, len);
printf("\n");
print3(arr, len);
return 0;
}
运行结果:
分析上述代码:数组名arr是数组首元素的地址,可赋值给p,所以在这里数组名和p是等价的,我们可以使用arr[i]访问数组,同理也可以使用p[i]访问数组。
同理 *(p + i) 与 p[i]也是等价的,arr[i] 等价于 *(arr + i),我们由此可以知道数组元素的访问时,也是转化成首元素的地址 + 偏移量求出元素的地址,然后进行解引用访问到元素本身。
10.一维数组传参的本质
一维数组传参的本质:本质上数组传参传的是数组首元素的地址。所以函数形参部分理论上应该使用指针变量来接收首元素的地址。由此我们知道在函数内部使用sizeof计算数组的大小是行不通的,在函数内部使用sizeof(arr)计算的是一个地址的大小(字节)并不是数组的大小(字节)。
#include <stdio.h>
void test(int* a)//参数是指针形式
{
printf("%d\n", sizeof(a));//计算一个指针变量的大小
}
void test2(int a[])//参数是数组,本质上是指针
{
printf("%d\n", sizeof(a));
}
int main()
{
int arr[10] = { 0 };
int sz = sizeof(arr) / sizeof(arr[0]);
printf("%d\n", sz);
//指针传参
test(arr);
//数组传参
test2(arr);
return 0;
}
运行结果:
总结:一维数组传参,形参部分可以是数组形式,也可以是指针的形式。
11.冒泡排序
冒泡排序的核心思想:两两相邻元素进行比较。
#include <stdio.h>
void bubble_sort(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz - 1; i++)
{
int j = 0;
for (j = 0; j < sz - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[10] = { 10,9,8,7,6,5,4,3,2,1 };
int sz = sizeof(arr) / sizeof(arr[0]);
printf("排序前:");
print_arr(arr, sz);
bubble_sort(arr, sz);
printf("排序后:");
print_arr(arr, sz);
return 0;
}
//优化之后
#include <stdio.h>
void bubble_sort(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz - 1; i++)
{
int flag = 1;//假设这一趟是有序的
int j = 0;
for (j = 0; j < sz - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
flag = 0;//产生交换,说明无序
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
if (flag == 1)//这一趟没交换说明全部有序,直接跳出循环
{
break;
}
}
}
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[10] = { 10,9,8,7,6,5,4,3,2,1 };
int sz = sizeof(arr) / sizeof(arr[0]);
printf("排序前:");
print_arr(arr, sz);
bubble_sort(arr, sz);
printf("排序后:");
print_arr(arr, sz);
return 0;
}
运行结果:
12.二级指针
指针变量也是变量,是变量就有地址,指针变量的地址存放在二级指针中。
二级指针运算:
*p :对p中的地址进行解引用,找到的是a,*p2访问的是a。
int a = 100;
int* p = &a;
**p2 :先通过*p2找到p,再对p进行解引用:*p,找到a.
int a = 100;
int* p = &a;
int** p2 = &p;
13.指针数组
指针数组是指针还是数组呢?
类比:
整型数组 - 存放整型的数组;
字符数组 - 存放字符的数组;
那么指针数组 - 存放指针的数组。
指针数组的每个元素都是用来存放指针(地址)的。
指针数组的每个元素是地址,又可以指向同一块区域。
13.1 用指针数组实现二维数组
#include <stdio.h>
int main()
{
int arr1[4] = { 1,2,3,4 };
int arr2[4] = { 2,3,4,5 };
int arr3[4] = { 3,4,5,5 };
//数组名是数组首元素的地址,类型是int*,可以存放到数组arr中
int* arr[3] = { &arr1,&arr2,&arr3 };
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 4; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
运行结果:
arr[i] 访问arr数组的元素,arr[i] 中的数组元素指向整型一维数组,arr[i][j]访问的是一维数组中的元素。
注意:上述代码虽模拟出二维数组的效果,实际上不是二维数组,因为每一行元素的地址不是连续的。
14.字符指针变量
指针类型中有一种指针类型为字符指针 char*
两种使用方式:
#include <stdio.h>
int main()
{
//方式一
char c = 'h';
char* pc = &c;
printf("pc = %c\n", *pc);
*pc = 'y';
printf("pc = %c\n", *pc);
//方式二
const char* cc = "hello world";
printf("%s\n", cc);
return 0;
}
运行结果:
需要注意的是:这里的const char* cc = "hello world"; 并不是把字符串"hello world"放在字符指针cc中,本质上是把字符串的首字符地址放在字符指针中。
下面学习一下 "字符数组和字符指针"的区别:
#include <stdio.h>
int main()
{
char str1[] = "hello world";
char str2[] = "hello world";
const char* str3 = "hello world";
const char* str4 = "hello world";
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和str地址不同,所以不相等;str3和str地址相同,所以相等。
15.数组指针变量
15.1 数组指针变量是什么?
指针数组是一种数组,数组中存放的是指针(地址)。
那么数组指针变量是指针变量?还是数组呢? --- 指针变量
类比:
整型指针变量:int* p; 存放的是整型变量的地址,能够指向整型数据的指针。
字符指针变量:char* c; 存放的是字符变量的地址,能够指向字符数据的指针。
数组指针变量是:存放数据的地址,能够指向数组的指针变量。
数值指针变量的形式:
int (*p)[10];
分析:p先和 * 结合,说明p是一个指针变量,然后指向的是10个整数的数组,所以p是一个指针,指向一个数组,叫数组指针。
注意:[ ] 的优先级要高于 * 号,所以必须加上 ( ) 来 保证 p 先和 * 结合。
15.2 数组指针变量初始化
数组指针变量是用来存放数组地址的,获取数组的地址使用 - &数组名。
int arr[10] = {0};
&arr;//得到数组的地址
如果要存放数组的地址,就得存放在数组指针变量中,例如:
int arr[4] = {1,2,3,4};
int (*p)[4] = &arr;
通过调试能看到 &arr 和 p的类型是完全一致的。
数组指针类型解析:
16.二维数组传参的本质
以前我们用二维数组传参给函数时,是下面这种写法:
#include <stdio.h>
void test(int arr[3][4], int r, int c)
{
int i = 0;
for (i = 0; i < r; i++)
{
int j = 0;
for (j = 0; j < c; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][4] = { {1,2,3,4},{2,3,4,5},{3,4,5,6} };
test(arr, 3, 4);
return 0;
}
能看到实参是二维数组,传给函数的形参也是二维数组的形式,是否还有其他写法?
有的,二维数组本质上每个元素是一维数组的数组,也就是二维数组的每个元素是一个一维数组。那么二维数组的首元素就是它的第一行(一维数组)。
数组名是首元素的地址我们可以得出,二维数组的数组名表示的是第一行的地址,是一维数组的地址 。上述代码中,第一行的一维数组是 int [4], 故第一行的地址的类型就是数组指针的类型int(*)[5]。二维数组传参本质上也是传递地址,传递的是第一行一维数组的地址,说明形参也是可写成指针形式。如:
#include <stdio.h>
void test(int(*p)[4], int r, int c)
{
int i = 0;
for (i = 0; i < r; i++)
{
int j = 0;
for (j = 0; j < c; j++)
{
printf("%d ",*(*(p+i)+j) );
}
printf("\n");
}
}
int main()
{
int arr[3][4] = { {1,2,3,4},{2,3,4,5},{3,4,5,6} };
test(arr, 3, 4);
return 0;
}
代码运行结果:
总结:二维数组传参,形参的部分可写成数组形式,也是写成指针形式。
17.函数指针变量
概念:函数指针变量是用来存放函数地址的,通过地址能调用函数。
可以测试一个函数是否有地址:
#include <stdio.h>
void test()
{
printf("hh");
}
int main()
{
printf("test = %p\n", test);
printf("&test = %p\n", &test);
return 0;
}
运行结果:
函数的地址打印出来了,所以函数名就是函数的地址,也可以通过&函数名获取函数的地址。
如果要将函数的地址存储起来,就得创建指针变量,函数指针变量的写法和数组指针类似。
例如:
函数指针类型分析:
17.2 函数指针变量的使用
通过函数指针调用指针指向的函数:
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int main()
{
int(*ptr)(int, int) = add;//方式一
int(*ptr)(int, int) = &add;//方式二
int ret = ptr(3, 4);
printf("ret = %d\n", ret);
return 0;
}
输出结果:
17.3 函数指针两个复杂例子
代码1
(*(void(*)())0)();
首先区分各个括号(* (void ( * ) ( ) ) 0 ) ( ),然后进行拆分,从中间拆得到void(*)(),表示这是一个无参无返回值的函数指针类型,放在0前面,表示把0强制类型转换成这个函数指针类型,即0变成函数指针,代码现在简化为(*0)(),其中0是函数指针类型,对0进行解引用找到这个函数,因为0是个函数指针类型void(*)( ),对0解引用传递的函数参数是空().
代码2
void(*signal(int,void(*)(int)))(int);
看这段代码,先分解一下,signal(int,void(*)(int))暂且用x代替,得到void(*x)(int),显然这个是一个函数指针的定义,说明signal(int,void(*)(int))是一个函数指针,signal是一个函数,它的返回值一个函数指针,它的参数类型是int,和函数指针void(*)(int)。
上面这种写法看着很复杂,可以使用typdef简化这段代码:typedef void (*n) (int);对这类函数指针重命名,最终的代码为 n signal(int,n); 看着是不是比上面代码更容易理解了。
17.3.1 typedef 关键字
typedef 是用来对类型重命名,把复杂类型,简单化。
例如:你可以把 unsigned int 类型写 uint.
typedef unsigned int uint;//把unsigned int 重命名为uint
当然指针类型也可以重命名,例如:将 char* 重命名为 ch:
typedef char* ch;
数组指针重命名:
例如:将数组指针类型int(*)[4],需要重命名为arr_t,可以这样写:
typedef int(*arr_t)[4];//新的类型名必须在*的右侧
函数指针类型的重命名也是一样的,例如:将void(*)(char)类型重命名为 pf_v,可以这样描述:
typedef void(*pf_v)(char);//新的类型名必须在*的右侧
18.函数指针数组
数组是一个存放相同数据类型的内存空间。把函数的地址存到一个数组中,那么这个数组被称为函数指针数组,这个数组的定义方式如下:
int (*p1[5])();
分析:p1 先和 [ ] 结合,说明p1是数组,数组的内容int(*)() 类型的函数指针。
19.函数指针数组的用法
举例:实现一个计算器,一般的写法:
//实现一个计算器
#include <stdio.h>
int add(int x, int y)
{
return x + y;
}
int sub(int x, int y)
{
return x - y;
}
int mul(int x, int y)
{
return x * y;
}
int div(int x, int y)
{
return x / y;
}
void menu()
{
printf("-------------------------------\n");
printf("--------- 1.add 2.sub -------\n");
printf("--------- 3.mul 4.div -------\n");
printf("--------- 0.exit -------\n");
printf("-------------------------------\n");
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;
do
{
menu();
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");
break;
default:
printf("选择错误,重新选择\n");
break;
}
} while (input);
return 0;
}
我们使用函数数组的写法:
//实现一个计算器
#include <stdio.h>
int add(int x, int y)
{
return x + y;
}
int sub(int x, int y)
{
return x - y;
}
int mul(int x, int y)
{
return x * y;
}
int div(int x, int y)
{
return x / y;
}
void menu()
{
printf("-------------------------------\n");
printf("--------- 1.add 2.sub -------\n");
printf("--------- 3.mul 4.div -------\n");
printf("--------- 0.exit -------\n");
printf("-------------------------------\n");
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;
int(*pf[5])(int x, int y) = {0,add,sub,mul,div};
do
{
menu();
printf("请选择功能:");
scanf("%d", &input);
if (input >= 1 && input <= 4)
{
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
ret = pf[input](x, y);
printf("ret = %d\n", ret);
}
else if (input == 0)
{
printf("退出计算器\n");
break;
}
else
{
printf("选择错误,重新输入\n");
}
} while (input);
return 0;
}
20.回调函数
概念:回调函数就是一个通过函数指针调用的函数。
如果把函数的指针(地址)作为参数传给另一个函数,当这个指针被用来调用其所指向的函数时,被调用的函数就是回调函数。注意:回调函数并不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另一方调用,用来对该事件或条件进行响应。
还是用计算器举例,在上面将计算器的实现代码中,还是有一些代码是重复出现的,是否有更简化的写法。答案是有的,上述代码中只有调用的代码的逻辑上有差异,其他都类似,所以我们可以 把调用的函数的地址以参数的形式传递过去,使用函数指针进行接收,函数指针指向什么函数就调用什么函数,这里使用的就是回调函数的功能。
//实现一个计算器
#include <stdio.h>
int add(int x, int y)
{
return x + y;
}
int sub(int x, int y)
{
return x - y;
}
int mul(int x, int y)
{
return x * y;
}
int div(int x, int y)
{
return x / y;
}
int Calc(int(*p)(int, int))
{
int x = 0;
int y = 0;
int ret = 0;
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
ret = p(x, y);
printf("ret = %d\n", ret);
}
void menu()
{
printf("-------------------------------\n");
printf("--------- 1.add 2.sub -------\n");
printf("--------- 3.mul 4.div -------\n");
printf("--------- 0.exit -------\n");
printf("-------------------------------\n");
}
int main()
{
int input = 0;
do
{
menu();
printf("请选择功能:");
scanf("%d", &input);
switch (input)
{
case 1:
Calc(add);
break;
case 2:
Calc(sub);
break;
case 3:
Calc(mul);
break;
case 4:
Calc(div);
break;
case 0:
printf("退出计算器\n");
break;
default:
printf("选择错误,重新选择\n");
break;
}
} while (input);
return 0;
}
21.qsort库函数的使用
qsort函数可以对任意类型数据排序,头文件时<stdlib.h>
qsort函数原型:
void qsort( void *base, size_t num, size_t width, int (__cdecl *compare )(const void *elem1, const void *elem2 ) );
解释每个参数的含义:
- void *base: 指向数组的起始地址值,通常该位置传入的时一个数组名
- size_t num:表示当前数组的元素个数
- size_t width:表示该数组中每个元素的大小(字节数)
- int(_cdecl *compare)(const void* elem1,const void *elem2):指向比较函数的函数指针,决定了排序的顺序。
21.1 使用qsort函数排序整型数据
//qsort排序整型数据
#include <stdio.h>
int cmp(const void* p1, const void* p2)
{
return *(int*)p1 - *(int*)p2;
}
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[] = { 3,1,5,9,6,2,4,8,7,0 };
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
printf("排序前:\n");
print_arr(arr, sz);
qsort(arr, sz, sizeof(arr[0]), cmp);
printf("排序后:\n");
print_arr(arr, sz);
return 0;
}
代码运行结果:
21.2 使用qsort排序结构体
#include <stdio.h>
#include <stdlib.h>
//定义结构体
struct Stu
{
char name[20];//名字
int age;//年龄
};
int cmp_stu_by_name(const void* p1, const void* p2)
{
return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}
int cmp_stu_by_age(const void* p1, const void* p2)
{
return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;
}
void print_Stu(struct Stu* s, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%s %d\n", s->name, s->age);
s++;
}
}
//按名字进行比较
void test1()
{
struct Stu st[] = { {"张三",24},{"李四",20},{"王五",18} };
int sz = sizeof(st) / sizeof(st[0]);
qsort(st, sz, sizeof(st[0]), cmp_stu_by_name);
print_Stu(st, sz);
}
//按年龄进行比较
void test2()
{
struct Stu st[3] = { {"张三",24},{"李四",20},{"王五",18} };
int sz = sizeof(st) / sizeof(st[0]);
qsort(st, sz, sizeof(st[0]), cmp_stu_by_age);
print_Stu(st, sz);
}
int main()
{
//按名字进行比较
//test1();
//按年龄进行比较
test2();
return 0;
}
按年龄比较的运行结果:
按名字比较的运行结果:
22.模拟实现qsort函数
使用回调函数,模拟实现qsort - 使用冒泡形式。
#include <stdio.h>
int cmp_int(const void* p1, const void* p2)
{
return (*(int*)p1) - (*(int*)p2);
}
void swap_arr(void* p1, void* p2,int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
char tmp = *((char*)p1 + i);
*((char*)p1 + i) = *((char*)p2 + i);
*((char*)p2 + i) = tmp;
}
}
void bubble(void* base, int sz, int width, int(*cmp)(void*, void*))
{
int i = 0;
for (i = 0; i < sz - 1; i++)
{
int j = 0;
for(j = 0 ; j < sz - i - 1; j++)
{
if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
{
swap_arr((char*)base + j * width, (char*)base + (j + 1) * width, width);
}
}
}
}
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[] = { 5,2,4,1,7,3,8,6,9,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
print_arr(arr, sz);
bubble(arr, sz, sizeof(arr[0]), cmp_int);
print_arr(arr, sz);
return 0;
}
运行结果:
23.sizeof 与 strlen 的区别
23.1 sizeof详解
概念:计算变量所占内存空间大小,单位是字节。如果操作数是类型,那么计算的是使用类型创建的变量所占内存空间的大小。
sizeof只计算占用内存空间的大小,不在乎内存中存放什么数据。
#include <stdio.h>
int main()
{
int a = 0;
printf("%d\n", sizeof(a));//4
printf("%d\n", sizeof(int));//4
//sizeof()是单目操作符 ,如下面这行代码即使没有括号,照样不影响计算
printf("%d\n", sizeof a);//4
return 0;
}
23.2 strlen详解
strlen 是C语言库函数,作用:求字符串长度。
函数原型:
size_t strlen(const char* str):
统计的是从strlen函数的参数str 这个起始地址往后,'\0'之前字符串中字符的个数。
strlen 函数会一直查找'\0'字符,直到找到为止,所以可能会越界查找。
比较下面这两种字符串的写法:
#include <stdio.h>
int main()
{
char arr1[5] = { 'h','e','l','l','o' };
char arr2[] = "hello";
printf("%d\n", sizeof(arr1));
printf("%d\n", sizeof(arr2));
printf("-----------------\n");
printf("%d\n", strlen(arr1));
printf("%d\n", strlen(arr2));
return 0;
}
代码运行结果:
23.3 sizeof 与 strlen对比
sizeof
- sizeof 是操作符
- sizeof计算操作数所占内存的大小,单位是字节
- 不关注内存中存放什么数据
strlen
- strlen是库函数,使用需要包含头文件 string.h
- strlen是求字符串长度的,统计的是 '\0' 之前的字符的个数
- 关注内存是否有 '\0' ,没遇到'\0',会持续向后找,可能会越界
24. 数组和指针试题
24.1 一维数组
#include <stdio.h>
int main()
{
int arr[] = {1,3,5,7};
//sizeof(数组); --- 得到的是整个数组的大小
printf("%d\n", sizeof(arr));//16
//arr是首元素的地址 - 类型是int *, arr+0 还是首元素的地址,指针(地址)是4(32位平台) / 8(64位平台)
printf("%d\n", sizeof(arr + 0));// 4 / 8
//arr是首元素的地址,*arr = 首元素,int型大小 == 4个字节
printf("%d\n", sizeof(*arr));// 4
//arr是首元素的地址,类型是int*,arr+1 是跳过一个整型,arr+1 是第二个元素的地址,指针(地址)大小是4 / 8
printf("%d\n", sizeof(arr + 1));// 4 / 8
//arr[1]类型是int型,int型大小是4个字节
printf("%d\n", sizeof(arr[1]));// 4
//&arr是 数组的地址,数组的地址也是地址(指针),大小是4/8个字节
printf("%d\n", sizeof(&arr));// 4/8
//*& 相互抵消,sizeof(*&arr) == sizeof(arr) = 16个字节,计算的是数组的大小
printf("%d\n", sizeof(*&arr));// 16
//&arr是整个数组,&arr+1 跳过整个数组后面那个位置的地址,是地址,那么大小是4/8个字节
printf("%d\n", sizeof(&arr + 1));// 4/8
//&arr[0],表示首元素的地址,指针(地址)的大小是4/8个字节
printf("%d\n", sizeof(&arr[0]));// 4/8
//&arr[0] + 1 -- 数组第二个元素的地址,指针(地址)的大小是4/8个字节
printf("%d\n", sizeof(&arr[0] + 1));// 4/8
return 0;
}
运行结果:
24.2 字符数组
代码1:
#include <stdio.h>
int main()
{
char arr[] = { 'h','e','l','l','o' };
//arr是首元素的地址,数组中没有\0,就会导致越界访问,结果就是随机值
printf("%d\n", strlen(arr));//随机值
//arr + 0表示首元素的地址,数组中没有\0,就会导致越界访问,结果就是随机值
printf("%d\n", strlen(arr + 0));//随机值
//arr是首元素的地址,*arr就是首元素,这里是'h','h'的ASCII码值是‘104’,
//相当于把104作为地址传给了strlen,strlen得到的就是野指针,代码是有问题的
//printf("%d\n", strlen(*arr));//err
//arr[1] -- 'e' -- 101,传给strlen函数也是有问题的
//printf("%d\n", strlen(arr[1]));//err
//&arr是数组的地址,起始位置是数组第一个元素的位置,随机值 x
printf("%d\n", strlen(&arr));//随机值 x
//数组的地址+1,跳过一个数组,指向后面的位置,随机值 x - 5
printf("%d\n", strlen(&arr + 1));//随机值 x - 5
//从第二个元素开始向后统计到'\0'前的位置
printf("%d\n", strlen(&arr[0] + 1));//随机值 x - 1
return 0;
}
比如:在我这运行的结果是这样的:
代码2:
#include <stdio.h>
int main()
{
char arr[] = { 'h','e','l','l','o' };
//sizeof(数组名) ,数组名单独放在sizeof内部,计算的是数组的大小(单位:字节)
printf("%d\n", sizeof(arr));//5
//arr - 数组名,表示数组的首元素地址,arr+0还是首元素的地址,指针(地址)的大小是 4/8个字节
printf("%d\n", sizeof(arr + 0));// 4/8
//arr是首元素地址,*arr就表示首元素,大小是1个字节
printf("%d\n", sizeof(*arr));//1
//arr[1]表示第二个元素,大小是1个字节
printf("%d\n", sizeof(arr[1]));//1
//&arr 是数组的地址,数组的地址也是地址 ,指针(地址)的大小是 4/8个字节
printf("%d\n", sizeof(&arr));// 4/8
//&arr+ 1跳过一个字符型数组,指向的是数组后面的位置,4/8个字节
printf("%d\n", sizeof(&arr + 1));// 4/8
//&arr[0] 表示首元素的地址 + 1,表示指向第二个元素的地址,指针(地址) 的大小是 4/8个字节
printf("%d\n", sizeof(&arr[0] + 1));// 4/8
return 0;
}
运行结果:
代码3:
#include <stdio.h>
int main()
{
char arr[] = "hello";
//arr是数组名,单独放在sizeof内部,计算的是数组的总大小,包括'\0'
printf("%d\n", sizeof(arr));//6个字节
//arr表示数组首元素的地址,arr+0还是首元素的地址,指针(地址)的大小 -- 4/8个字节
printf("%d\n", sizeof(arr + 0));// 4/8个字节
//arr表示数组首元素地址,*arr表示首元素,char型 大小是是1个字节
printf("%d\n", sizeof(*arr));//1个字节
//arr[1]表示第二个元素,大小是1个字节
printf("%d\n", sizeof(arr[1]));//1个字节
//&arr 是数组的地址,指针(地址) 的大小是4/8个字节
printf("%d\n", sizeof(&arr));// 4/8个字节
//&arr 是数组的地址,&arr+1就是跳过整数组,指向数组后面的位置,还是地址,指针(地址)大大小是4/8个字节
printf("%d\n", sizeof(&arr + 1));// 4/8个字节
//&arr[0]是数组首元素的地址 + 1,表示第二个元素的地址,指针(地址)的大小是 -- 4/8个字节
printf("%d\n", sizeof(&arr[0] + 1));// 4/8 个字节
return 0;
}
运行结果:
代码4:
#include <stdio.h>
int main()
{
char arr[] = "hello";
//strlen 统计'\0'之前的字符
printf("%d\n", strlen(arr));//5个字节
//arr是元素的地址,arr+0还是首元素的地址,在'\0'之前有5个字符
printf("%d\n", strlen(arr + 0));//5个字节
//*arr -- 数组首元素,'h' -- 104 -->err
//printf("%d\n", strlen(*arr));//err
arr[1] -- 第二个元素,'e' -- 101 -->err
//printf("%d\n", strlen(arr[1]));//err
//&arr是数组的地址,从前向后找,5个字节
printf("%d\n", strlen(&arr));//5个字节
printf("%d\n", strlen(&arr + 1));//随机值
//从第2个元素向后找到'\0'之前
printf("%d\n", strlen(&arr[0] + 1));//4个字节
return 0;
}
运行结果:
代码5:
int main()
{
char* p = "hello";
//p是指针变量,计算指针变量的大小,4/8个字节
printf("%d\n", sizeof(p));// 4/8个字节
//p+1是'e'的地址,指针(地址)的大小是4/8个字节
printf("%d\n", sizeof(p + 1));// 4/8个字节
//p的类型是char*,*p就是char类型,char类型大小是1个字节
printf("%d\n", sizeof(*p));//1个字节
//p[0 -->*p -->'a' ,大小是1个字节
printf("%d\n", sizeof(p[0]));//1个字节、//&p是取出数组地址,指针(地址)的大小是4/8个字节
printf("%d\n", sizeof(&p));// 4/8个字节
//&p+1是跳过p指针变量,指向后面的地址,地址大小是4/8个字节
printf("%d\n", sizeof(&p + 1));// 4/8个字节
//&p[0] - 取出字符串首元素的地址,+1是第二个字符的地址,大小是4/8个字节
printf("%d\n", sizeof(&p[0] + 1));// 4/8个字节
return 0;
}
运行结果:
代码6:
#include <stdio.h>
int main()
{
char* p = "hello";
printf("%d\n", strlen(p));//5个字节
printf("%d\n", strlen(p + 1));//4个字节
//printf("%d\n", strlen(*p));//p是首元素的地址,*p是首元 -- 'h' -- 104 ;//err
//printf("%d\n", strlen(p[0]));//p[0]是首元素 == *(p+0) == *p //err
printf("%d\n", strlen(&p));///&p是指针变量p的地址,和字符串没关系,
//从p这个指针变量的起始位置向后计数,p变量存放的地址是未知的,所以这是一个随机值
printf("%d\n", strlen(&p + 1));//随机值
printf("%d\n", strlen(&p[0] + 1));//4个字节 &p[0] 取出字符串首元素的地址,+1是第二个字符的位置,向后统计到'\0'之前的字符数
return 0;
}
运行结果
24.3 二维数组
int main()
{
int a[3][4] = { 0 };
//a是数组名,单独放在sizeof内部,计算的是数组的大小,单位是字节 -- 48 = 3*4*sizeof(int);
printf("%d\n", sizeof(a));//48
//a[0][0]是第一行的数组名,大小是4个字节
printf("%d\n", sizeof(a[0][0]));//4
//a[0] 表示的是第一行的数组名,数组名单独放在sizeof内部,计算的是数组的总大小,16个字节
printf("%d\n", sizeof(a[0]));//16
//a[0] + 1 == a[0][0],数组第一行第一个元素的的地址,指针(地址)的大小是4/8个字节
printf("%d\n", sizeof(a[0] + 1));//4/8
//*(a[0]+1)表示第一行第二个元素,int型大小是4个字节
printf("%d\n", sizeof(*(a[0] + 1)));// 4
//a作为数组名并没有单独放在sizeof内部,a表示数组首元素的地址,是二维数组首元素的地址,
//也就是第一行的地址,a+1跳过一行,指向了第二行,所以a+1是第二行的地址,a+1是数组指针,指针(地址)大小是4/8个字节
printf("%d\n", sizeof(a + 1));// 4/8
//a+1是第二行的地址,*(a+1)就是第二行,计算的是第二行的大小 - 16个字节
printf("%d\n", sizeof(*(a + 1)));//16
//a[0]是第一行的数组名,&a[0]取出的就是数组的地址,就是第一行的地址
//&a[0]+1,就是第二行的地址,指针(地址)的大小是4/8个字节
printf("%d\n", sizeof(&a[0] + 1));//4/8个字节
//*(&a[0]+1) 表示对第二行的地址解引用,访问的是第二行,大小是16个字节
printf("%d\n", sizeof(*(&a[0] + 1)));//16
//a作为数组名没有单独放到sizeof内部,a表示数组首元素的地址,是二维数组首元素的地址,
// 也就是第一行的地址,*a就是第一行,计算的是第一行的大小,16个字节
printf("%d\n", sizeof(*a));//16
//a[3]是第四行的数组名,单独放在sizeof内部,计算的是第四行的大小,16个字节
printf("%d\n", sizeof(a[3]));//16
//说明:a[3]可以不是真实存在,仅仅通过类型就能推断出长度。
return 0;
}
运行结果:
数组名的含义:
- sizeof(数组),这里的数组名表示整个数组,计算的是整个数组的大小。
- &数组名,这里的数组表示整个数组,取出的是整个数组的地址。
- 除此之外所有的数组名都表示首元素的地址。
25.指针运算分析
25.1 题型1
#include <stdio.h>
int main()
{
int arr[5] = { 1,2,3,4,5 };
int* p = (int*)(&arr + 1);
printf("%d %d\n", *(arr + 1), *(p - 1));//输出结果:2 5
//分析:arr 表示的是首元素的地址,arr+1 表示第二个元素地址,解引用就是2
//&arr表示数组的地址 + 1,跳过一个整型数组,指向数组后面的空间,-1指向数组最后一个元素
return 0;
}
25.2 题型2
#include <stdio.h>
//在32位环境下,假设结构体的大小是20个字节
//下面程序输出的结果是什么
struct Test
{
int Num;
char* Name;
short s;
char c[2];
short sh[4];
}*p = (struct Test*)0x100000;
//指针+-整数
int main()
{
//结构体指针+1,表示跳过一个结构体 1 + 20 == 14(十六进制)
//0x100000 + 14 = 0x100014
printf("%p\n", p + 0x1);//00100014
//p强制类型转换位无符号的整型,p+0x1 == p+1 == 1 (整型值+1,就是+1)
//0x100000 + 1 -->0x100001
printf("%p\n", (unsigned long)p + 0x1);//00100001
//p强制类型转换成无符号整型指针,p+0x1 = p + 4 = 4
//0x100001 + 4 --> 00100004
printf("%p\n", (unsigned int*)p + 0x1);//00100004
return 0;
}
运行结果:
25.3 题型3
#include <stdio.h>
int main()
{
//存储在arr数组中的元素为3、7、11、0、0, 原因在于中间包裹元素的符号是()不是{ }
//下面这种写法是逗号表达式,只会取最后出现的元素
int arr[3][2] = { (1,3),(5,7),(9,11) };
int* p;
p = arr[0];
printf("%d\n", p[0]);
//arr[0] 是第一行的数组名,数组名表示首元素的地址
//也就是&arr[0][0]的地址,这里取出的元素是 3
//*(p+0) == *p
return 0;
}
25.4 题型4
#include <stdio.h>
//在x86环境下,程序输出的结果是什么?
int main()
{
int a[5][5];
int(*p)[4];//p是一个数组指针,p指向的数字是四个整型元素
p = a;
printf("%p %d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
return 0;
}
运行结果:
代码分析:
25.5 题型5
#include <stdio.h>
int main()
{
int a[2][5] = { {1,2,3,4,5},{6,7,8,9,10} };
int* p1 = (int*)(&a + 1);//&a表示数组地址,+1跳过整个数组,指向数组后面的空间
int* p2 = (int*)(*(a + 1));//*(a+1) == a[1] == 6
printf("%d %d", *(p1 - 1), *(p2 - 1));//10 5
return 0;
}
代码分析:
25.6 题型6
#include <stdio.h>
int main()
{
char* a[] = { "work","at","tenxun" };
char** pa = a;
pa++;
printf("%s\n", *pa);//at
return 0;
}
代码分析:
25.7 题型7
#include <stdio.h>
int main()
{
char* c[] = { "ENTER","NEW","POINT","FIRST" };
char** cp[] = { c + 3,c + 2,c + 1,c };
char*** cpp = cp;
printf("%s\n", **++cpp);
printf("%s\n", *-- * ++cpp + 3);
printf("%s\n", *cpp[-2] + 3);
printf("%s\n", cpp[-1][-1] + 1);
return 0;
}
代码分析:
看到这里,指针已经学习完了。相信你已经掌握了指针的相关概念,突破一个大知识点。学习是没有尽头的,祝大家学习进步,共勉~