一 单位转换
1Byte = 8Bit 1Kb = 1024Byte 1Mb = 1024Kb ... 1Pb = 1024Tb
1个比特位和存储一个2进制位0或1
二 指针介绍
内存单元的编号 == 地址 == 指针 名字不一样,意思一样
计算机中的编址,并没有把每个字节的地址记录下来,而是通过硬件设计完成的。好比钢琴,制造商已经把乐器的各个信息(哆来咪发)在硬件上面设计好了,并且所有演奏者都知道,本质是一种约定出来的共识。那么地址也是一样,需要用的时候,就从内存中获取,获取后,CPU也能识别。
int a = 10; //变量的本质就是向内存申请空间,向内存申请4个字节的空间,存放数据10
int* p = &a; //<*> p是指针变量(存放指针); <int>指针变量p所指向的对象a的类型是int;<int *>是p的指针类型
*p = 1; //*p解引用操作符/间接访问操作符;通过p中存放的地址,找到所指向的a
printf("%d\n", a); //很多时候a不方便自己修改自己,就用解应用来修改自己,实现的目的一样。
三 指针变量大小
printf("%zd\n", sizeof(char*)); // x86是4 x64是8
printf("%zd\n", sizeof(double*)); //x86是4 x64是8无论是哪个指针类型,在x86环境下都是4个字节; 在x64环境下都是8个字节;
四 指针的意义
1,指针的类型决定了对指针解应用的权限,如char*类型只能访问1个字节,而int*类型可以访问4个字节;
2,利用指针打印数组的每一个元素;
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr; //&arr[0]
for (int i = 0; i < sz; i++)
{
//printf("%d ", *p++); //p[i]或*(arr+i)或i[p];
printf("%d ", *p);
p++;
}
3,const修饰符
const 修饰变量,使得变量不能被修改,但是使用解应用操作符可以修改。 【示例2】
const 放在左边,限制解应用操作符(*p)修改地址,但是变量p可以修改。 【示例1】
const 放在右边,限制变量p不能修改,但是解应用操作符(*p)可以被修改。【示例3】
左边修饰: const int *p 和 int const *p一样;
右边修饰: int * const p
示例1:定义常量n,const放在左边限制解应用操作符,变量p仍然可以修改
const int n = 100;
const int* p = &n;
// *p = 10; //报错;
// n = 10; //报错;
p = 80; //修改p的值并不会影响n的值;
示例2:const 修饰常量,解应用操作符正常
const int n = 100;
int* p = &n;
* p = 20; //通过解应用操作符仍然修改了const定义的常量值
printf("%d\n", n); //20
示例3 const 修饰右边,解应用操作符正常,变量p无法被修改
const int n = 100;
int* const p = &n;
* p = 20; //通过解应用操作符仍然修改了const定义的常量值
// p = 30; //报错
printf("%d\n", n); //20
4,指针运算
4.1,指针+整数
p 是变量本身,里面存的是地址; *p 是p所指向的对象,就是值;
// 指针打印数组元素
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = &arr;
for(int i=0;i<sz;i++)
{
printf("%d ", *(p + i));
}
// 指针打印字符串元素
char arr[] = "hello";
char* ch = arr;
while (*ch!='\0')
{
printf("%c ", *ch++);
}
4.2,指针-指针
两个指针指向同一块空间,取绝对值得到指针和指针之间元素的个数
int arr[10] = { 0 };
int ret = &arr[9] - &arr[0]; //9
4.3,strlen求字符串长度,用指针的方式写出strlen的功能
int my_strlen(char* str)
{
int count = 0;
while (*str!='\0')
{
count++;
str++;
}
return count;
}
int my_strlen1(char* str)
{
// a b c d e f '\0' //str就是a的位置
char* start= str; //记录第一个位置
while (*str != '\0')
{
str++;
}
return str - start; // 最后一个'\0'的位置减去第一个,得到一共有多少个元素
}
int main()
{
char arr[] = "abcdef";
int 直接求 = strlen(arr);
int 指针求1 = my_strlen(arr);
int 指针求2 = my_strlen1(arr);
printf("%d %d %d", 直接求, 指针求1, 指针求2); // 6 6 6
return 0;
}
4.4,指针的关系运算
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<sz+arr) //指针大小比较,最大+最小arr[0]
{
printf("%d ", *p++);
}
五 野指针,位置未知
造成原因:1,指针未初始化;2,越界;
示例1:未初始化
int* p;
*p = 20;
示例2:下标越界
int arr[10] = { 0 };
int* p = &arr[0];
for (int i = 0; i <= 11; i++)
{
*(p++) = i;
}
六 如何规避野指针
1,如果知道指针指向哪里,就直接赋值地址;
int a = 10; int* p = &a;
2,如果不知道指针指向哪里,就赋值NULL(保留数)
int* ptr = NULL; //空指针,NULL本质就是0
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int* p = arr; //arr 和 &arr[0]是一样的,都是表示数组的首元素 int i = 0; int sz = sz = sizeof(arr) / sizeof(arr[0]); for (i = 0; i < sz; i++) { printf("%d", *p++); } p = NULL; //指针不再使用要及时置NULL,只要指针是NULL就不会去访问它 //... p = arr; //后面如果还想用,就重新赋值即可; if (p != NULL) { //... }
3,防止下标越界;
4,避免使用返回局部变量的地址;
int* test() { // arr是局部变量,局部变量都放在栈上,返回栈空间的地址很容易造成野指针 int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; // ... return arr; //相当于首元素 } int main() { int* p = test(); //这里的p就是野指针,p再找回arr的时候,就会出问题 return 0; }
assert 断言,需要assert.h头文件定义宏assert()
int a = 10; int* p = &a; if (p != NULL) //每次都判断就会很不方便 { //.... }
#define NDEBUG #include<assert.h> int main() { int a = 10; int* p = NULL; //故意赋值NULL assert(p != NULL); //条件不成立,直接报错 // 条件不成立,但是又不想让他报错,则在assert.h加入NDEBUG,就相当于设置开关功能. //用在非指针上只要条件不满足也会报错,如assert(p) return 0; }
七 传址调用 和 传值调用
要修改外部变量的值,就需要用传址调用,否则传值调用即可。
示例1:写一个函数,来交换两个整形变量的值
void Swape(int x, int y)
{
int z = 0;
z = x;
x = y;
y = z;
}
int main()
{
int a = 10, b = 20;
printf("a=%d b=%d \n", a, b); // 10 20
// 形参是实参的临时拷贝,有自己独立的空间,传值调用时,对形参的修改不影响实参
Swape(a, b);
printf("a=%d b=%d \n", a, b); //10 20 值并没有改变
return 0;
}
上面的代码,形参交换了变量,但是实参并没有改变,如果想改变实参的值就需要传址
void Swapz(int* px, int* py)
{
int z = 0;
z = *px;
*px = *py;
*py = z;
}
int main()
{
int a = 10, b = 20;
printf("a=%d b=%d \n", a, b);
Swapz(&a, &b); //传址,传过去是首元素地址
printf("a=%d b=%d \n", a, b);
return 0;
}
通过上面的代码,传址的方式,变量a和b就实现了交换
示例2: 求两个数的最大值
int Max(int x, int y)
{
if (x > y)
return x;
else
return y;
}
int main()
{
int a = 10;
int b = 20;
int m = Max(a, b);
printf("max=%d \n", m);
return 0;
}
八 数组名的理解
数组名是数组首元素的地址,但是有两个例外:
1,sizeof(数组名) //整个数组的大小,单位字节。
2,&数组名,表示整个数组的地址;
int arr[10] = { 0 }; int a = sizeof(arr); // a = 40 int b = &arr; int c = &arr + 1; printf("a=%d b=%d c=%d", a, b, c); // a=40;b = 78642328;c = 78642368 相差40
除此之外,数组名就是首元素的地址。
int arr[10] = { 0 }; printf("%d \n", &arr[0]); // 502266376 printf("%d \n", &arr[0]+1); // 502266380 //相差1个元素 printf("%d \n", arr); // 502266376 printf("%d \n", arr + 1); // 502266380 //相差1个元素
2,使用指针访问数组
int arr[3] = { 0 }; int sz = sizeof(arr) / sizeof(arr[0]); int* p = arr; //首元素; for (int i = 0; i < sz; i++) { scanf("%d", p++); //p+i 或 p++ 都是指向数组元素; } p = arr; //本来p指向最后1个元素,因此让p重新指向第1个元素; for (int i = 0; i < sz; i++) { printf("%d ", *(p + i)); //p是首元素地址加上下标为i的元素 }
int arr[3] = { 0 }; int sz = sizeof(arr) / sizeof(arr[0]); int* p = arr; //首元素; for (int i = 0; i < sz; i++) { scanf("%d", p++); //p+i 或 p++ 都是指向数组元素; } p = arr; //重新指向第一个元素 for (int i = 0; i < sz; i++) { printf("%d ", *(p + i)); //p是首元素地址加上下标为i的元素 } int arr[3] = { 1,2,3 }; int* p = arr; int sz = sz = sizeof(arr) / sizeof(arr[0]); // 方式1 for (int i = 0; i < sz; i++) { printf("%d ", *p); p++; } // 方式2 for (int i = 0; i < sz; i++) { // printf("%d ", *p++); // printf("%d ", *(p + i)); //p为首元素地址加上i下标 // printf("%d ", *(arr + i)); // *p=arr 那么p就是arr // printf("%d ", p[i]); // printf("%d ", i[arr]); // arr[i] ==== *(arr+i) //等价,编译器在处理arr[i]的时候也会变成*(arr+i)后才去找数组里面的元素的. // *(p+i) ==== p[i] //由上面可以得出下面相等 // 经过刚刚分析得出... // arr[i] =等于= *(arr+i) =加法交换= *(i+arr) =可以写成= i[arr] } // 验证p+i 是不是 下标i的地址 for (int i = 0; i < sz; i++) { printf("%p ========%p \n", p + i, &arr[i]); //打印值一模一样 }
九 一维数组传参
数组传过去的本质就是指针,传过去首元素的地址,可以写成数组形式或指针形式;
void print(int* p, int sz) //int p[] { for (int i = 0; i < sz; i++) { //printf("%d ", *p++); printf("%d ", p[i]); // *(p+i) 解应用操作符的值 } } int main() { int arr[] = { 1,2,3,4 }; int sz = sizeof(arr) / sizeof(arr[0]); print(arr,sz); //arr其实是第一个元素的地址 return 0; }
十 冒泡排序
int sort(int* arr, int sz) // 写成数组也可以 int arr[] { for (int i = 0; i < sz - 1; i++) //只需要交换sz-1次即可,最后一个不用交换 { // 一趟冒泡排序 int flag = 1; //假设数组元素是有序的 for (int j = 0; j < sz - i - 1; j++) //每次比较sz-1-i次 { if (arr[j] > arr[j + 1]) { int temp = arr[j]; // 交换 arr[j] = arr[j + 1]; arr[j + 1] = temp; flag = 0; //赋值是0说明还有需要交换的 } } if (flag == 1) //没有交换的数据,退出循环更高效 break; } } void print(int* p, int sz) { for (int i = 0; i < sz; i++) { printf("%d ", *p++); } } int main() { int arr[] = { 1,2,3,6,5,4,7,8,9 }; int sz = sizeof(arr) / sizeof(arr[0]); sort(arr, sz); //传址排序 print(arr, sz); //传址打印 return 0; }
十一 二级或多级指针(多级指针很少用)
int a = 10; //a是整形变量,a有自己的地址,&a就是a所占4个字节的第一个字节的地址
int* p=&a; //p是指针变量,p也是有自己的地址,&p就拿到了p的地址
int * *pp = &p; //pp也是指针变量,二级指针变量也是变量
int*** ppp = &pp; //三级指针
十二 指针数组
类比:整形数组-存整形的数组; int arr[10];
字符数组-存字符的数字; int ch[10];
指针数组-存指针的数组; int *parr[5]; char *pc[10];int a = 1, b = 2, c = 3; int* parr[3] = { &a,&b,&c }; int i = 0; for (i = 0; i < 5; i++) { printf("%d ",*(parr[i])); } // 上面的方式也可以,但是很少使用,对地址解引用就可以得到内容 int arr1[] = { 1,2,3,4,5 }, arr2[] = { 3,4,5,6,7 }, arr3[] = { 5,6,7,8,9 }; int* parr[] = { arr1,arr2,arr3 }; //得到的都是首元素的地址; for (int i = 0; i < 3; i++) { for (int j = 0; j < 5; j++) //三种写法都可以拿到内容打印 { // printf("%d ", parr[i][j]); // printf("%d ", *(*(parr + i)+j)); // printf("%d ", (*(parr + i))[j]); } printf("\n"); }
十三 数组指针
1,字符串数组
const char* p = "abcdef"; //const左修饰,解应用失效,变量p仍然可以修改。首元素a的地址存放在指针变量p中 // printf("%c\n", *p); // a // p ="efg"; //二次赋值变量p // printf("%s\n", p); // p存放的是首元素地址,字符串是连续存放的,打印直到遇到'\0'才停止 // ----------------------------------------------- char str1[] = "hello"; char str2[] = "hello"; const char* str3 = "hello"; const char* str4 = "hello"; // str1和str2是不同数组,指向元素首元素地址也不一样;str3和str4是常量,值也无法修改.
2,数组指针
数组指针是什么呢?
字符指针-指向字符的指针,存放字符的地址; char ch='w';char *pc=&ch;
整形指针-指向整形的指针,存放整形的地址; int num=10; int*p=#
数组指针-指向数组的指针,存放数组的地址; int arr[10]; int (*p)[10]=&arr;
int arr[6] = { 1,2,3,4,5,6 }; int(*p)[6] = &arr; // 数组指针; for (int i = 0; i < 6; i++) { printf("%d", (*p)[i]); }
3,二维数组传参的本质
正常数组的打印
int arr[3][5] = { {1,2,3,4,5},{7,8,9,1,2}, {3,4,5,6,7} }; int r = 3, c = 5; for (int i = 0; i < r; i++) { for (int j = 0; j < c; j++) { printf("%d ", arr[i][j]); } printf("\n"); }
void test(int(*p)[5], int r, int c) //接收参数也要是数组指针 { for (int i = 0; i < r; i++) { for (int j = 0; j < c; j++) { // printf("%d ", p[i][j]); // printf("%d", *(*(p + i)+j)); // printf("%d", *(p[i] + j)); // printf("%d", (*(p + i))[j]); } printf("\n"); } } int main() { int arr[3][5] = { {1,2,3,4,5},{7,8,9,1,2}, {3,4,5,6,7} }; // 二维数组名也是首元素地址,代表二维数组第一个元素{1,2,3,4,5},二维数组在内存中也是连续存放的. test(arr, 3, 5);//实参是首元素地址数组,形参接受也要是数组地址; return 0; }
4,函数指针变量
int Add(int x, int y)
{
return x + y;
}
int main()
{
// 三 函数指针变量
// 数组指针 - 是指针 - 是存放数组地址的指针
// 函数指针 - 是指针 - 是指向函数地址的指针
// printf("%p\n", &Add); //对于函数,&函数名和函数名拿到的地址都是函数地址
// printf("%p\n", Add);
int (*pf)(int, int) = &Add; //函数指针变量pf; //int是返回值类型,如果void则这里也要写void
int r = (*pf)(3, 5);
int r1 = (pf)(3, 5); //这个*也可以不写,但是不容易被理解.
printf("%d %d", r, r1);
return 0;
}
5,typedef 关键字,用来类型重命名,将复杂的类型简单化。
typedef unsigned int uint; //简化前 unsigned int;
typedef int(*parr_t)[5]; //新的类型名必须在*的右边
typedef void(*pfun_t)(int); //新的类型名必须在*的右边
void(*signal(int, void(*)(int)))(int); //可以简化成下面的代码;
typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);
6,函数指针数组
存放函数指针的数组; 函数参数必须是一致的。
int (*p1[3])(int, int) = { 0, Add, Sub };
int m=p1[1](5, 8); //调用
示例:转移表(参数个数必须一致且返回值类型一致)
// 三种方式写加减乘除运算
void eunm()
{
printf("************************\n");
printf("*** 1 加法 2减法 ***\n");
printf("*** 3 乘法 4 除法 ***\n");
printf("*** 0 退出 ***\n");
printf("************************\n");
}
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mut(int x, int y)
{
return x * y;
}
int Dut(int x, int y)
{
return x / y;
}
void calc(int (*p)(int, int))
{
int x = 0, y = 0, ret = 0;
printf("请输入两个运算数>:");
scanf("%d %d", &x, &y);
ret = p(x, y);
printf("运算结果:%d\n", ret);
}
int main2() //函数指针方式1
{
int input = 0, x = 0, y = 0, result = 0;
do
{
eunm();
int (*arr[5])(int, int) = { 0,Add,Sub,Mut,Dut };
printf("请选择>:");
scanf("%d", &input);
if (input >= 1 && input <= 4)
{
printf("请输入两个数>:");
scanf("%d %d", &x, &y);
result=arr[input](x, y);
printf("运算结果:%d\n", result);
}
else if (input == 0)
{
printf("退出游戏");
}
else
{
printf("输入错误,请重新输入\n");
}
} while (input);
return 0;
}
int main() //函数指针方式2
{
int input = 0, x = 0, y = 0;
do
{
int result = 0;
eunm();
printf("请选择>:");
scanf("%d", &input);
switch (input)
{
case 1:
calc(Add);
break;
case 2:
calc(Sub);
break;
case 3:
calc(Mut);
break;
case 4:
calc(Dut);
break;
case 0:
printf("退出游戏");
break;
default:
printf("输入错误!\n");
break;
}
} while (input);
}
int main1() //正常写法,比较啰嗦
{
int input = 0, x = 0, y = 0;
do
{
int result = 0;
eunm();
printf("请选择>:");
scanf("%d", &input);
switch (input)
{
case 1:
printf("请输入两个运算数>:");
scanf("%d %d", &x, &y);
result = Add(x, y);
printf("加法结果是:%d\n", result);
break;
case 2:
printf("请输入两个运算数>:");
scanf("%d %d", &x, &y);
result = Sub(x, y);
printf("减法结果是:%d\n", result);
break;
case 3:
printf("请输入两个运算数>:");
scanf("%d %d", &x, &y);
result = Mut(x, y);
printf("乘法结果是:%d\n", result);
break;
case 4:
printf("请输入两个运算数>:");
scanf("%d %d", &x, &y);
if (y == 0)
{
printf("被除数不能为0\n");
}
else
{
result = Dut(x, y);
printf("除法结果是:%d\n", result);
}
break;
case 0:
printf("退出游戏");
break;
default:
printf("输入错误!\n");
break;
}
} while (input);
}