第一节:指针是什么
//指针其实就是地址,地址就是编号
//指针就是内存单元的编号
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能找到这块空间,但不能使用
int* p = test();//虽然这里的指针p储存了变量a的地址,但是因为上面提到的原因,往下使用会出现问题。
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;
int i = 0;
for (i = 0; i < 4; i++)
{
*(p + i) = 0;
}
for (i = 0; i < 5; i++)
{
printf("%d ", arr[i]);
}
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. 在小端机器中,下面代码输出的结果是:( )
int main()
{
int a = 0x11223344;
char* pc = (char*)&a;
*pc = 0;
printf("%x\n", a);
return 0;
}
//A.00223344
//B.0
//C.11223300
//D.112233
//答案:C
6. 逆序字符串
#include <string.h>
int main() {
char arr[10000] = { 0 };
gets(arr);
char* left = arr;
char* right = left + (strlen(arr) - 1);
while (left < right) {
char tmp = *left;
*left = *right;
*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;//*(pulPtr + 3) = *(pulPtr + 3) + 3;
printf("%d,%d\n", *pulPtr, *(pulPtr + 3));
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
#include <math.h>
int main()
{
int a = 0;
scanf("%d", &a);
int ret = 0;
int sum = 0;
int i = 0;
for ( i = 0; i < 5; i++)
{
ret = ret * 10 + a;
//ret += (int)(a * pow(10, i));
sum += ret;
}
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;//0~100000的数
for (i = 0; i < 10000; i++)
{
int n = 1;//位数
int j = i;
//while (j)
//{
// j /= 10;
// n++;
//}
while (j /= 10)
{
n++;
}
j = i;
int sum = 0;
while (j)
{
sum += (int)(pow((j % 10), n));
j /= 10;
}
if (i == sum)
printf("%d ", i);
}
return 0;
}
13. 打印菱形
// *
// ***
// *****
// *******
// *********
// ***********
//*************
// ***********
// *********
// *******
// *****
// ***
// *
int main()
{
int n = 0;
scanf("%d", &n);
//上半部分1~7行
int i = 0;
int j = 0;
for (i = 0; i < n; i++)
{
//打印空格
for (j = 0; j < n - i - 1; j++)
{
printf(" ");
}
//打印星
for (j = 0; j < 2 * i + 1; j++)
{
printf("*");
}
printf("\n");
}
//下半部分8~13行
for (i = 0; i < n - 1; i++)
{
//打印空格
for (j = 0; j <= i; j++)
{
printf(" ");
}
//打印星
for (j = 0; j < 2 * (n - 1 - i) - 1; j++)
{
printf("*");
}
printf("\n");
}
return 0;
}