C语言-第五章:指针与数组

传送门:C语言-第四章:操作符

目录

第一节:初识指针

        1-1.数据存储初阶

        1-2.内存地址

        1-3.指针

                1-3-1.指针的基本认识

                1-3-2.指针的使用

​编辑

                1-3-3.变量类型与权限 

                1-3-4.指针的运算

                        1-3-4-1.指针+-整数

                         1-3-4-2.指针-指针

                1-3-5.野指针问题

第二节、数组

        2-1.基本认识

        2-2.数组的初始化

        2-3.数组的存储

        2-4.数组名的本质

        2-5.数组元素的使用

        2-6.数组遍历

加餐-第三节:ASCLL码值

下期预告


第一节:初识指针

        指针是由指针类型定义的变量,它与变量的数据存储有关,接下来先讲一讲数据存储相关知识。

        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、二维数组

传送门:C语言-第五章-加餐:冒泡排序与二维数组

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值