第一节:指针是什么
//指针其实就是地址,地址就是编号
//指针就是内存单元的编号
int main()
{
int a = 10;//a是整形变量,占用4个字节的内存空间。
int* pa = &a;//pa是一个指针变量,用来存放地址的。口头语中经常说成指针
//&a(取地址a)取到的是第一个字节的地址
//指针本质就是地址
//口语中说的指针,其实是指针变量,指针变量就是一个变量,指针变量是用来存放地址的变量
return 0;
}
第二节:指针和指针类型
1. 不同类型的指针
int main()
{
char* pc = NULL;
short* ps = NULL;
int* pi = NULL;
double* pd = NULL;
//sizeof 返回值的类型是无符号整形 unsigned int
printf("%zu\n", sizeof(pc));//使用%zu最准确,本身就是给sizeof准备的一种格式
printf("%zu\n", sizeof(ps));
printf("%zu\n", sizeof(pi));
printf("%zu\n", sizeof(pd));
return 0;
}
2. 指针 + - 整数
指针的类型决定了指针向前或者向后走一步有多大(距离)。
int main()
{
int a = 0x11223344;
int* pi = &a;
char* pc = (char*)&a;
printf("pi = %p\n", pi);
printf("pi+1 = %p\n", pi+1);
printf("pc = %p\n", pc);
printf("pc+1 = %p\n", pc+1);
//结论2:
//指针的类型决定了指针+1或-1操作的时候,跳过了几个字节
//决定了指针的步长
return 0;
}
指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。 比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。
int main()
{
//2个十六进制数字就是1个字节
//1个十六进制数字能翻译成4个二进制位
//0 1 2 3 4 5 6 7 8 9 a b c d e f - 16进制数字
//如果用二进制表示16进制数字,当数字较大时就需要用到4位,例如10(即十六进制的a)用二进制表示就是1010
int a = 0x11223344;
int* pi = &a;
*pi = 0;
char* pc = (char*)&a;//可以存的下
*pc = 0;//但是改不了
//结论1:指针类型决定了指针在被解引用的时候访问几个字节
//int*的指针,解引用访问4个字节
//char*的指针,解引用访问1个字节
return 0;
}
同样步长也不能混用
int main()
{
int a = 0;
int* pi = &a;//pi 解引用访问4个字节,pi+1跳过4个字节
float* pf = &a;//pf 解引用访问4个字节,pf+1跳过4个字节
//int* 和 float* 是否可以通用?- 不能
//因为站在pi的角度,它认为它指向内存放的是整形数据
//因为站在pf的角度,它认为它指向内存放的是浮点数数据
//整数和浮点数在内存的存储方式有差异
//下方代码存到内存的数据是完全不同的
*pi = 100;//0x00000064
*pf = 100.0;
return 0;
}
第三节:野指针
野指针成因:
1. 指针未初始化
//1. 指针未初始化
int main()
{
int* p;
//p没有初始化,就意味着没有明确的指向
//一个局部变量不初始化,放的是随机值:0xcccccccc
*p = 10;//非法访问内存,这里的p就是野指针
return 0;
}
2. 指针越界访问
//2. 指针越界访问
int main()
{
int arr[10] = { 0 };
int* p = arr;//arr 等效 &arr[0]
int i = 0;
for (i = 0; i < 11; i++)
{
*p = i;
p++;//当指针指向的范围超出数组arr的范围时,p就是野指针
}
return 0;
}
3. 指针指向的空间释放
//3. 指针指向的空间释放
int* test()
{
int a = 10;
return &a;
}
int main()
{
//因为a是局部变量,出了函数就销毁了。
//虽然p能找到这块空间,但不能使用
//虽然这里的指针p储存了变量a的地址,但是因为上面提到的原因,往下使用会出现问题。
int* p = test();
return 0;
}
4. 避免返回局部变量地址
int* test()
{
int a = 10;
return &a;
}
int main()
{
//虽然变量已经销毁了,但还是能够通过这个指针(非法地址)找到这块空间,但是已经没有这块空间的使用权限了
//如果这块空间没有被再次使用,那还是10,但不意味永远都是10
//假如在函数调用后再打印个hehe,这里*p就不一定是10了
int* p = test();
if (p != NULL)//其实这个判断失效了
{
printf("%d\n", *p);
}
return 0;
}
5. 如何规避野指针
int main()
{
//明确给指针初始化
int a = 10;
int* p = &a;
*p = 20;
//如果不知道明确的值,就初始化为空指针
int* p2 = NULL;//NULL本质是0
*p2 = 100;//err,没有指向有效空间,0不允许访问
//此方法不能避免刚才函数(即空间释放)的野指针
int* p3 = NULL;
if (p3 != NULL)//等效 if (p3)。但建议前面写法,因为直观
{
*p3 = 100;//ok
}
return 0;
}
第四节:指针运算
1. 指针+ -整数
例子1
int main()
{
#define N_VALUES 5
float values[N_VALUES];
float* vp;
//指针+-整数;指针的关系运算
for (vp = &values[0]; vp < &values[N_VALUES];)
{
*vp++ = 0;
//等效下方
//*vp = 0;
//vp++;
//*vp++和(*vp)++的区别
//*vp++ : *vp其实没有对指向对象做什么操作,然后让vp向后走一步。这个是地址++
//(*vp)++ : 先对vp解引用(找到vp所指向的对象),然后++。这个是vp指向对象++
}
return 0;
}
例子2
int main()
{
int arr[10] = { 0 };
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
//数组下标写法
//for (i = 0; i < sz; i++)
//{
// arr[i] = 1;
//}
//int* p = arr;
//for (i = 0; i < sz; i++)
//{
// *p = 1;
// p++;
//}
//等效上方
int* p = arr;
for (i = 0; i < sz; i++)
{
*(p + i) = 1;//i第一次等于0,就等于没加,还是指向第一个元素
}
return 0;
}
2. 指针 - 指针
int main()
{
int arr[10] = { 0 };
printf("%d\n", &arr[9] - &arr[0]);//9
printf("%d\n", &arr[0] - &arr[9]);//-9
//指针减去指针的绝对值 得到的是指针和指针之间元素的个数
//不是所有的指针都能相减
//指向同一块空间的2个指针才能相减
int arr[10] = { 0 };
char ch[5] = { 0 };
printf("%d\n", &ch[0] - &arr[5]);//err 没有逻辑,结果是不可预知的
return 0;
}
其他例子
//版本1 - 指针向后移动
int my_strlen(char* str)
{
int count = 0;
while (*str != '\0')
{
count++;
str++;
}
return count;
}
//版本2 - 递归
int my_strlen(char* str)
{
if (*str != '\0')
return 1 + my_strlen(str + 1);
else
return 0;
}
//版本3 - 指针-指针
int my_strlen(char* str)
{
char* start = str;//获取字符串首字母的地址
while (*str != '\0')
{
str++;
}
return (str - start);
}
int main()
{
int len = my_strlen("abcdef");//这里传参传的是字符串首字符的地址
printf("%d\n", len);
//没有指针+指针,没有意义。可以把指针想象成日期。日期可以加减天数,日期可以减日期,但是日期相加没有意义
return 0;
}
3. 指针的关系运算
//指针的关系运算
int main()
{
#define N_VALUES 5
float values[N_VALUES] = { 1 };
float* vp;
//版本1
for (vp = &values[N_VALUES]; vp > &values[0];)
{
//这里是前置--,所以上方vp只能大于。
//如果是>=,当vp指向第一个元素的时候,条件依然满足,
//那么进入循环,vp--指向了第一元素的前面并赋值,这时就越界访问
*--vp = 0;
//等效下方
//--vp;
//*vp = 0;
}
int i = 0;
for (i = 0; i < 5; i++)
{
printf("%f ", values[i]);
}
//版本2 - 优化版本
//实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。
for (vp = &values[N_VALUES - 1]; vp >= &values[0]; vp--)//不建议这样写
{
*vp = 0;
}
//上方两种写法都越界了,但是依据下方规定,建议第一种写法
//标准规定:
//允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,
//但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
return 0;
}
第五节:指针和数组
1. 通过指针访问数组
//数组:一组相同类型的元素集合
//指针变量:一个变量,存放的是地址
int main()
{
int arr[10] = { 0 };
//arr是首元素地址
//&arr[0]
int* p = arr;
//通过指针来访问数组
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i < sz; i++)
{
//p=arr,可以写成*(arr+i)
printf("%d ", *(p + i));//p指向第一个元素,+i跳过i个元素
}
//数组名表示的是数组首元素的地址
for (i = 0; i < sz; i++)
{
printf("%p ----- %p\n", &arr[i], (p + i));//如果p指向这个数组的首元素地址,那么两个地址一样
}
//arr[i] -> *(arr+i)
//arr[i]本质上计算的时候,还是通过数组名先加i找到下标i元素(arr[i]就是下标i那个元素的地址)然后解引用
//arr[i]是形式,本质上是*(arr+i)
return 0;
}
2. 数组传参的2种方式
//数组传参 - 指针形式
void test(int* p, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
}
//数组形式
void test(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);//arr[i] -> *(arr+i)
}
}
int main()
{
int arr[10] = { 0 };
test(arr, 10);
return 0;
}
第六节:二级指针
int main()
{
//int a = 10;
pa是一个指针变量,一级指针变量。
即通过pa去找a一次就能够找到(对pa做一次解引用就找到a)
//int* pa = &a;//*说明pa是指针,前面的int说明pa指向对象的类型是int类型
//*pa = 20;
//printf("%d\n", a);
//pa是变量,pa在内存中也有自己的空间
int a = 10;
int* pa = &a;//这颗*说明pa是指针,int说明pa指向的对象是int类型
//第二颗* 说明ppa是指针。前面的int*说明ppa指向的对象pa的类型是int*类型
int** ppa = &pa;//ppa是一个二级指针变量。
**ppa = 20;//解引用一次得到是pa,再解引用一次才是
printf("%d\n", a);
//二级指针变量是用来存放一级指针变量的地址
return 0;
}
第七节:指针数组
是存放指针的数组
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
int c = 30;
int arr[10];
int* pa = &a;
int* pb = &b;
int* pc = &c;
//parr就是存放指针的数组 - 指针数组
int* parr[10] = { &a,&b,&c };
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%d ", * (parr[i]));
}
return 0;
}
用二级指针模拟二维数组
int main()
{
//二维数组
int arr[3][4] = { 1,2,3,4,2,3,4,5,3,4,5,6 };
//1 2 3 4
//2 3 4 5
//3 4 5 6
int i = 0;
int j = 0;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 4; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
//模拟
int arr1[4] = { 1,2,3,4 };//arr1数组名相当于元素1的地址
int arr2[4] = { 2,3,4,5 };//arr2数组名相当于元素2的地址
int arr3[4] = { 3,4,5,6 };//arr3数组名相当于元素3的地址
int* parr[3] = { arr1,arr2,arr3 };
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 4; j++)
{
printf("%d ", parr[i][j]);//arr[i] <==> *(arr+i) []就等于解引用
}
printf("\n");
}
}
作业
1. 关于指针的概念,错误的是:()
A.指针变量是用来存放地址的变量
B.指针变量中存的有效地址可以唯一指向内存中的一块区域
C.野指针也可以正常使用
D.局部指针变量不初始化就是野指针
答案:C
2. 以下系统中,int类型占几个字节,指针占几个字节,操作系统可以使用的最大内存空间是多大:()
A.32位下:4, 4, 2 ^ 32 64位下:8, 8, 2 ^ 64
B.32位下:4, 4, 不限制 64位下:4, 8, 不限制
C.32位下:4, 4, 2 ^ 32 64位下:4, 8, 2 ^ 64
D.32位下:4, 4, 2 ^ 32 64位下:4, 4, 2 ^ 64
答案:C
3. 使用指针打印数组内容
写一个函数打印arr数组的内容,不使用数组下标,使用指针。
arr是一个整形一维数组。
//版本一
void print_arr(int* arr, int sz)
{
int i = 0;
for ( i = 0; i < sz; i++)
{
printf("%d ", *(arr + i));
}
}
//版本二
void print_arr(int* arr, int sz)
{
int* q = arr;
while (q < arr + sz)
{
printf("%d ", *q++);
}
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
print_arr(arr, sz);
return 0;
}
4. 下面代码的结果是:( )
int main()
{
int arr[] = { 1, 2, 3, 4, 5 };
short* p = (short*)arr;//shor*指针一次能访问两个字节。+1一次跳过两个字节
//0x 00 00 00 01 - 1的十六进制
//0x 00 00 00 02 - 2的十六进制
//0x 00 00 00 03 - 3的十六进制
//0x 00 00 00 04 - 4的十六进制
//0x 00 00 00 05 - 5的十六进制
//数据在内存中存放的时候,有一个顺序问题。大小端字节序。实际存放情况如下:
//01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 05 00 00 00
//左边是低地址,右边是高地址
//因为int类型是四个字节,short*指针一次访问两个字节,
//所以下方循环4次,总共改了8个字节,即前两个整数
int i = 0;
for (i = 0; i < 4; i++)
{
*(p + i) = 0;
}
for (i = 0; i < 5; i++)
{
printf("%d", arr[i]);//0 0 3 4 5
}
return 0;
}
//A.1 2 3 4 5
//B.0 0 3 4 5
//C.0 0 0 0 5
//D.1 0 0 0 0
//答案:B
5. 在小端机器中,下面代码输出的结果是:( )
//0x11223344在内存顺序是44332211。因为修改了第一个字节,所以修改了44为00
int main()
{
int a = 0x11223344;
char* pc = (char*)&a;
*pc = 0;
printf("%x\n", a);//0x11223300
//任何一个变量或表达式都有2个属性:值属性;类型属性
//int a = 3;
//a + 4.5 -> 7.5(值属性)
//类型属性:double
//a的值是3,类型属性是int。 &a的类型属性是int*,a的值属性就是地址
return 0;
}
//A.00223344
//B.0
//C.11223300
//D.112233
//答案:C
6. 逆序字符串
#include <string.h>
int main()
{
char arr[10001] = {0};//加1是为了放下\0
//scanf("%s", &arr);//scanf遇到空格就不读取了
gets(arr);
int left = 0;//从下标0开始
int right = strlen(arr) - 1;
while (left < right)
{
char tmp = arr[left];
arr[left] = arr[right];
arr[right] = tmp;
left++;
right--;
}
printf("%s\n", arr);
return 0;
}
7. 下面关于指针运算说法正确的是:( )
A.整形指针 + 1,向后偏移一个字节
B.指针 - 指针得到是指针和指针之间的字节个数
C.整形指针解引用操作访问4个字节
D.指针不能比较大小
答案:C
8. 下列程序段的输出结果为( )
int main()
{
unsigned long pulArray[] = { 6,7,8,9,10 };
unsigned long* pulPtr;
pulPtr = pulArray;
*(pulPtr + 3) += 3;//下标为3的元素在加3
printf("%d, %d\n", *pulPtr, *(pulPtr + 3));//6 12
return 0;
}
//A.9, 12
//B.6, 9
//C.6, 12
//D.6, 10
//答案:C
9. 关于二级指针描述描述正确的是:( )
A.二级指针也是指针,只不过比一级指针更大
B.二级指针也是指针,是用来保存一级指针的地址
C.二级指针是用来存放数组的地址
D.二级指针的大小是4个字节
答案:B
10. 下面哪个是指针数组:( )
A. int* arr[10];
B.int* arr[];
C.int** arr;
D.int(*arr)[10];
答案:A
11. 计算求和
求Sn = a + aa + aaa + aaaa + aaaaa的前5项之和,其中a是一个数字,
例如:2 + 22 + 222 + 2222 + 22222
//2
//2*10+2 = 22
//22*10+2 = 222
//222*10+2 = 2222
int main()
{
int a = 0;
int n = 0;
scanf("%d %d", &a, &n);
int sum = 0;
int k = 0;//每一项
int i = 0;
for (i = 0; i < n; i++)
{
k = k * 10 + a;
sum += k;
}
printf("%d\n", sum);
return 0;
}
12. 打印水仙花数
求出0~100000之间的所有“水仙花数”并输出。
“水仙花数”是指一个n位数,其各位数字的n次方之和确好等于该数本身,
如 : 153=1 ^ 3+5 ^ 3+3 ^ 3,则153是一个“水仙花数”。
非函数版本
#include <math.h>int main()
{
int i = 0;
for (i = 0; i <= 100000; i++)//判断i是否为水仙花数
{
//1234
//1234/10=123 1位
//123/10=12 2位
//12/10=1 3位
//1/10=0 4位
//1. 计算i是几位数 -> n
int n = 1;//任何一个数至少是一位数
//下方有一个问题,i不停除以10直到等于0,但是上方又是遍历i,i要++。所以矛盾了
//这里是典型的循环内部改变循环变量,所以需要一个临时变量来存放i,这样就可以不修改i变量
int tmp = i;
int sum = 0;//sum不能创建在循环外面,因为每一个i都要求和验证,所以每次都要从0开始
while (tmp / 10)
{
n++;//如果i/10不等于0,位数就加1
tmp /= 10;
}
//2. 得到i的每一位,计算他的n次方之和
//上方经过循环,每次tmp最后都等于0,所以再次把i放到临时变量中,得到i的每一位
tmp = i;
//1234%10=4
//1234/10=123
//123%10=3
//123/10=12
//通过上述方法得到每一位的数,直到tmp变成0说明所有位都取出
while (tmp)
{
sum += pow(tmp % 10, n);//得到每一位,在计算它的n(位数)次方;即某一位的位数的次方
tmp /= 10;//算完某一位的次方后,再去掉那一位
}
if (sum == i)//判断每一位 位数次方的和是否等于它本身
{
printf("%d ", i);
}
}
return 0;
}
函数版本
//函数版本
int is_narcissistic_number(int i)
{
int n = 1;
//1. 计算i是几位数 -> n
int tmp = i;
int sum = 0;
while (tmp / 10)
{
n++;
tmp /= 10;
}
//2. 得到i的每一位,计算他的n次方之和
tmp = i;
while (tmp)
{
sum += pow(tmp % 10, n);//得到每一位,在计算它的n(位数)次方;即某一位的位数的次方
tmp /= 10;//算完某一位的次方后,再去掉那一位
}
return sum == i;//等效下方,如果sum等于i,表达式为真,返回1;如果不等于表达式为假,返回0
//if (sum == i)//判断每一位 位数次方的和是否等于它本身
// return 1;
//else
// return 0;
}
int main()
{
int i = 0;
for (i = 0; i <= 100000; i++)//判断i是否为水仙花数
{
if (is_narcissistic_number(i))
{
printf("%d ", i);
}
}
return 0;
}
13. 打印菱形
思路:分上下两个部分(上半部到第7行,即最多那一行)。上半部空格在减少,星星在增多;下半部分空格增多,星星减少
// *
// ***
// *****
// *******
// *********
// ***********
//*************
// ***********
// *********
// *******
// *****
// ***
// *
int main()
{
int line = 0;
scanf("%d", &line);
//上 - 上半部分是line行
int i = 0;
for (i = 0; i < line; i++)//上半部分的打印
{
//上半部每一行的打印,分为空格和*
//空格的打印 - 随着行的增长,空格数量减少。假设有7行,第一行有6个空格,依次减少。第7行没空格
int j = 0;
//line-1表示只有6行有空格。随着行数增加1,空格减少1,所以减去行数
for (j = 0; j < line - 1 - i; j++)
{
printf(" ");
}
//*的打印 - *和行的关系是2*i+1就是每行*的数量,i第一次等于0
for (j = 0; j < 2 * i + 1; j++)
{
printf("*");
}
printf("\n");
}
//下 - 下半部分是line-1行,最终打印的行数是2*line-1行
for (i = 0; i < line - 1; i++)//下半部分的打印
{
//打印一行
//下半部空格打印 - 随着行数增长,空格数量增加,第几行就有几个空格
int j = 0;
for (j = 0; j <= i; j++)
{
printf(" ");
}
//*的打印 - *和行的关系是,假设有6行,第一行是2*6-1,第二行是2*5-1
for (j = 0; j < 2*(line-1-i)-1; j++)
{
printf("*");
}
printf("\n");
}
return 0;
}
14. 使用指针打印数组内容
void print(int* p, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
printf("\n");
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
print(arr, sz);
return 0;
}