传送门:C语言-第四章:操作符
目录
第一节:初识指针
指针是由指针类型定义的变量,它与变量的数据存储有关,接下来先讲一讲数据存储相关知识。
1-1.数据存储初阶
第四章的位移操作符部分已经提到,变量被定义和初始化后就会在内存划分一块空间并存储它的值,
下面我们模拟一下变量在内存中的存储情况:
假如现在定义了一个 int 类型的局部变量a:
它会占据4个字节的空间,假如之后又定义了 char 类型的局部变量b,long long 类型的局部变量c:
可以看到,后定义的变量反而在内存的前面,这是因为局部变量保存在内存中名为 栈区 的区域,它的特点就是 先入后出 ,先定义的局部变量会被后定义的局部变量压在下面。
现在我们知道了计算机是如何存储变量的,那么计算机是怎么在内存中找到它的空间并使用其值的呢?下面我们就来解答这个问题。
1-2.内存地址
其实内存除了分成一格一格之外,还给这些格子从0开始给它们取了“门牌号”:
计算机可以通过 “门牌号” 和 类型 取到整个数据。
例如取用变量c:在程序运行之前计算机就会把变量c 解析成 它的第一个“门牌号”:8,然后根据其类型 long long 的大小 8字节,从8取到15,就取到了 变量 c 存储的所有数据。
而变量 c 的第一个“门牌号”就是它的 地址。
既然变量定义时就已经分配好空间了,那能不能在它定义之后,我们也使用 地址 和 类型 取到它存储的整个数据呢?答案是肯定的,它的奥妙就藏在接下来的内容——指针 中。
1-3.指针
1-3-1.指针的基本认识
指针是用指针类型定义的变量,可以用来找到其他变量存储的数据,获取或者修改它,定义它的方式如下:
[类型][*][指针名] = [&][变量]; // 类型:指针所存放的地址对应的变量的类型 // *:表示定义一个指针 // &:取地址,获得变量的“门牌号” // 例如: int a = 10; int *ptr = &a; // ptr 就保存了 a 的地址和类型,这叫做指针 ptr 指向变量 a // 如果不初始化可以把指针赋值为 NULL,表示不存放任何 地址 int *ptr = NULL;
指针也是一种变量,所以它也可以存储数据,它存储的数据就是地址,它自己的类型就和它指向的变量的类型对应。
在32位平台上,它的大小是4字节;在64位平台上,它的大小是8字节。
我的电脑就是64位平台,所以我们接下来只认为 指针 是8字节大小,验证指针类型的大小:
#include <stdio.h> int main() { int* ptr_1 = NULL; char* ptr_2 = NULL; long long* ptr_3 = NULL; printf("ptr_1: %d\n",sizeof(ptr_1)); printf("ptr_2: %d\n",sizeof(ptr_2)); printf("ptr_3: %d\n",sizeof(ptr_3)); return 0; }
不论指针指向变量的类型是什么,它的大小都是8字节,这是因为内存空间是有限的,8字节足够表示所有的地址,而且变量的类型与它自己的存放位置也无关。
对于地址,我们也不用十进制或者二进制表示它,而是用16进制:
#include <stdio.h> int main() { int a; int* ptr = &a; printf("%p\n",ptr); // 指针类型用 %p打印 return 0; }
上面就是一个16进制数,A代表10、B代表11、C代表12、以此类推。
1-3-2.指针的使用
认识指针后我们就可以使用它了,在指针前加 * 表示解引用,找到指针指向的变量。
我们直接来看它是怎么使用的:
#include <stdio.h> int main() { int a = 1; int* ptr = &a; // 指针的定义和初始化 printf("%d\n", *ptr); // 对指针解引用 return 0; }
打印了 a 的值,说明成功根据指针 ptr 找到了变量 a。
不仅如此,指针还能改变变量的值:
#include <stdio.h> int main() { int a = 1; int* ptr = &a; // 指针的定义和初始化 *ptr = 2; // 对指针接引用并赋值 printf("%d\n", a); return 0; }
变量 a 的值就从1 变成了 2。
1-3-3.变量类型与权限
变量的类型本质就是一种访问权限。
比如 int 类型的变量在使用时,它就只允许获取和修改4个字节的空间;
char 类型只允许获取和修改 1 个字节的空间;
duoble 类型只允许获取和修改 8 个字节的空间;
。。。。。。
指针就是根据 地址 找到变量的空间,然后根据类型在权限之内获取和修改数据,而不会影响到其他变量的使用。
理解了上面的内容,才能理解深刻变量的本质。
1-3-4.指针的运算
1-3-4-1.指针+-整数
虽然指针存放的地址是一个16进制整数,但是它加上(或减去)一个整数时并不是把地址与整数简单的相加减,而是把这个整数与指针的访问权限相乘后再直接加到地址上,下面写代码理解:
#include <stdio.h> int main() { int a = 1; int* ptr = &a; // 指针的定义和初始化 printf("%p\n", ptr); printf("%p\n", ptr+1); printf("%p\n", ptr+2); printf("%p\n", ptr+3); printf("%p\n", ptr+4); // ptr指向 int 类型的变量,大小是4个字节,每次增加的数应该是 1*4 return 0; }
只看最后两位:
84 -> 88 正好增加4;
88 -> 8C 正好增加4(C表示12);
8C -> 90 正好增加4;
90 -> 94 正好增加4;
假如指针指向 char 类型的数据呢?
#include <stdio.h> int main() { char a = 'A'; char* ptr = &a; // 指针的定义和初始化 printf("%p\n", ptr); printf("%p\n", ptr+1); printf("%p\n", ptr+2); printf("%p\n", ptr+3); printf("%p\n", ptr+4); // ptr指向 char 类型的变量,大小是1个字节,每次增加的数应该是 1*1 return 0; }
每次正好增加1。
指针加上(或减去)的这个整数(不乘权限),又叫做指针的 偏移量 ,它可以为正,可以为负。加时为正,减时为负。
1-3-4-2.指针减指针
两个指针的类型相同才能进行指针减指针的操作,而指针减指针也不是简单的把地址相减,它的作用是获取两个指针的偏移量:
#include <stdio.h> int main() { int a = 'A'; int* ptr_1 = &a; // 指针的定义和初始化 int* ptr_2 = ptr_1 + 3; int* ptr_3 = ptr_1 - 2; printf("ptr_2 - ptr_1 = %d\n", ptr_2 - ptr_1); printf("ptr_3 - ptr_1 = %d\n", ptr_3 - ptr_1); return 0; }
指针+指针没有意义,C语言也不支持这种语法。
1-3-5.野指针问题
野指针:指向的位置不可知或者非法的指针。
它可以由一下几种情况产生:
1、指针未初始化
#include <stdio.h> int main() { int* ptr; *ptr = 1; // 非法访问内存 return 0; }
因为内存在使用完之后不会把所有的比特位变成0,而是标记为空,下次使用再直接覆盖即可(这就是为什么卸载软件只需要一瞬间),所以变量在定义但是没有初始化或赋值时它的值是随机的,这就导致 ptr 指向一个随机的、很可能未开辟的空间。
如果解引用对这块空间进行访问,这是不安全的,编译器会阻止这种情况的发生。
2、指针指向的空间被销毁了(生命周期结束了)
#include <stdio.h> int* test() { int a = 0; return &a; } int main() { int* ptr = test(); // 函数返回时 a 被销毁了,即 ptr 指向一块没有开辟的空间 *ptr = 1; return 0; }
3、指针的越界访问
#include <stdio.h> int main() { char a = 'A'; int* ptr = &a; *ptr = 2; return 0; }
这就导致 ptr 非法访问了未开辟的3个字节的空间。
学习完指针的基础后,我们就可以来学习数组了。
第二节、数组
2-1.基本认识
数组是用来存放相同类型数据的一种容器,里面的每个数据称为这个数组的 元素。
数组的定义格式如下:
[类型] [数组名][数组元素个数]; // 例如: int arr[100];
类型:数组的元素的类型
arr:数组的名字,就像变量一样可以任意取
数组元素个数:这个数组最多可容纳的元素个数
2-2.数组的初始化
:
数组有几种初始化的情况,下面用 int 类型的数组举例子
1、完全初始化
int arr_1[5] = {1,2,3,4,5}; // 正好5个元素 int arr_2[] = {1,2,3,4,5}; // 省略数组元素个数,初始化时的元素个数就是数组元素个数
2、不完全初始化
int arr[5] = {1,2,3}; // 剩下的空位默认为0
3、char 类型数组的特殊初始化
char 类型数组又叫 字符串,它的初始化方式有一些不同:
char str[6] = {'w','o','r','l','d','\0'};// 完全初始化,'\0'是C语言中字符串的结束标志,每个字符串都有且也属于数组元素 char str[] = {'w','o','r','l','d','\0'}; // 同数组 char str[] = "world"; // 与上面等价,以这种方式初始化'\0'自动会被加入字符串结尾,不需要写出来
'\0'是字符串的结束标志,其他类型的数组没有也不需要结束标志
字符串用shuang'yi'hao
2-3.数组的存储
数组中的元素在内存中是连续存储的。
假如定义了一个 int 类型的数组 arr,它的元素是1、2、3、4、5,那么它的存储情况为:
而且相邻元素的地址差值与元素类型有关:
2-4.数组名的本质
数组名其实是一个指针,类型与元素类型相同,它指向数组的第一个元素:
#include <stdio.h> int main() { int arr[] = { 1,2,3,4,5 }; printf("%d",*arr); // 对数组名解引用 return 0; }
以下两种情况数组名才代表整个数组:
1、&数组名
&数组名意味取出整个数组的地址
#include <stdio.h> int main() { int arr[] = { 1,2,3,4,5 }; printf("%p\n", &arr); printf("%p\n",(&arr)+1); return 0; }
它们的差值正好为数组的大小20字节。
2、sizeof(数组名)计算数组大小
#include <stdio.h> int main() { int arr[] = { 1,2,3,4,5 }; printf("%d\n", sizeof arr); return 0; }
2-5.数组元素的使用
既然数组名指向首元素的地址,数组又是连续存储的,那么我们就可以用数组名+偏移量的方式获取之后的元素:
#include <stdio.h> int main() { int arr[] = { 1,2,3,4,5 }; printf("%d\n", *arr); // 偏移量为0 printf("%d\n", *(arr+1)); // 偏移量为1 printf("%d\n", *(arr+2)); // 偏移量为2 printf("%d\n", *(arr+3)); // 偏移量为3 printf("%d\n", *(arr+4)); // 偏移量为4 return 0; }
这个偏移量又叫元素的 下标,它从0开始,到数组元素个数-1结束,我们可以使用下标来直接访问这个元素,这需要用到 [ ] 操作符:
#include <stdio.h> int main() { int arr[] = { 1,2,3,4,5 }; printf("%d\n", arr[0]); // 等价于printf("%d\n", *arr); printf("%d\n", arr[1]); // 等价于printf("%d\n", *(arr +1)); printf("%d\n", arr[2]); // 等价于printf("%d\n", *(arr +2)); printf("%d\n", arr[3]); // 等价于printf("%d\n", *(arr +3)); printf("%d\n", arr[4]); // 等价于printf("%d\n", *(arr +4)); return 0; }
ps:arr[1]的本质就是*(arr+1),所以也可以反过来使用:1[arr],它们的效果是一样的
还可以修改这个元素:
#include <stdio.h> int main() { int arr[] = { 1,2,3,4,5 }; printf("%d\n", arr[0]); // 等价于printf("%d\n", *arr); printf("%d\n", arr[1]); // 等价于printf("%d\n", *(arr +1)); printf("%d\n", arr[2]); // 等价于printf("%d\n", *(arr +2)); printf("%d\n", arr[3]); // 等价于printf("%d\n", *(arr +3)); arr[4] = 6; // 修改元素值 printf("%d\n", arr[4]); // 等价于printf("%d\n", *(arr +4)); return 0; }
这样就把5改成了6。
注意:在使用下标访问元素时,如果下标超过了数组的范围,即:
arr[6] 并没有在数组中,它的本质是*(arr+6),对一块未开辟的内存进行了解引用,这也属于指针的越界访问。
2-6.数组遍历
上面我们一直用 printf 函数单独打印每个元素,当元素过多时就会很麻烦,我们可以用循环语句来减少工作量:
#include <stdio.h> int main() { int arr[100] = {0}; for (int i = 0; i < sizeof(arr)/sizeof(arr[0]); i++) // 用数组遍历给每个元素赋值 { arr[i] = i + 1; } for (int i = 0; i < sizeof(arr)/sizeof(arr[0]); i++) // 再一次遍历并打印元素的值 { printf("%d ",arr[i]); } return 0; }
ps:数组大小除以一个元素的大小就等于元素个数
字符串有自己的结束标志'\0',所以打印字符串时不需要循环,只要给 printf 函数传入字符串的数组名和打印类型 %s,他就会自动打印字符串直到遇见'\0':
#include <stdio.h> int main() { char str[] = "Hello World!"; printf("%s\n",str); return 0; }
加餐-第三节:ASCLL码值
计算机是以二进制的形式存储数据的,char 类型的变量也是如此,各种符号(包括字母,标点等)又都存储在 char 类型的变量中,这说明各种符号有其独特的二进制序列存储在内存中,二进制序列又可以转换成唯一的十进制整数,所以在计算机中,符号的本质也是整数,这个十进制整数就是符号的ASCLL码值。
下列是常见符号与它的ASCLL码值:
既然符号的本质是整数,那么它们可以比较大小 :
#include <stdio.h> int main() { if ('C' > 'A') // 字符与字符比较 { printf("字符C大于字符A\n"); } if ('C' > 0) // 字符与数字比较 { printf("字符C大于0\n"); } int a = 55; char c = 'C'; if (c > a) // char变量与int变量比较 { printf("c大于a\n"); } return 0; }
故字符型又叫字符整型,它也属于整型家族。
不仅如此,整数和字符之间还可以混用:
#include <stdio.h> int main() { char c = 65;// 65 是字符a的ASCLL码值 printf("c: %c\n", c); int a = 66; printf("66对应的字符是:%c\n", a); return 0; }
下期预告
下一次是加餐,主要内容如下:
1、冒泡排序
2、二维数组