每一次盘复,相信都会有一定的收获。 ----日向晚,声声慢
目录
一.数组:
数组是什么?数组是能存放同一种类型的集合。
组成部分:存放内容的类型 数组名 [常数]。
在c99中有变长数组的概念,变长数组指的就是,[]中可以是变量,但是变长数组在创建的时候不能初始化。在VS的编译器下不支持变长数组,一些刷题网站上是支持的,例如牛客网上。
这是在牛客网上,没有按照题目要求,但编译能通过。在使用变长数组的时候,一般通过循环来赋值,在强调一次,变长数组不能初始化。
1.数组的创建与初始化
比较字符串长度与元素个数:
字符串长度存在差异主要是因为strlen()在求字符串的长度时是寻找’ \0 ‘,计算在' \0 '之前的元素。
在求arr7的字符串长度时,在arr7[16]的这个位置找到了'\0'。
这里还要关注到我们数组元素个数的求法。下面稍后就会讲到。
2.数组内存中的存储
%p是打印地址。
为什么是相差4呢,因为我们数组的类型是int类型的,int类型占4个字节。
图片那里每一个空格代表的是4个字节。
数组在内存中的存储是连续的,并且随着下标的增长,地址也在由低到高的变化。
二.二维数组
组成部分:
存放内容的类型 数组名 [常数][常数] ( [ 行 ][ 列 ] )
也存在变长的数组。在使用变长数组的时候,赋值就用双层循环就可以了。
1.二维数组的创建与初始化
初始化
2.二维数组在内存中的存储
图中每个空格代表的是4个字节。
三.注意事项
1.越界访问-《C陷阱与缺陷》中的一道经典题型
题目是这样的,以下代码会出现什么问题?为什么?
#include <stdio.h>
int main()
{
int i = 0;
int arr[10] = {0};
for(i=0; i<=12; i++)
{
arr[i] = 0;
printf("hehe\n");
}
return 0;
}
答案是死循环。为什么呢?
我们先介绍一下内存。内存中有栈区,堆区,静态区。还有一些其他的,先讲这三个。
我们这里讲栈区
程序在执行的时候先是创建变量,再是创建数组。我们来看图
这段代码,两个问题越界和死循环。因为越界导致死循环。
我们在图中可以发现,死循环的原因是,arr[12]的地址就是i的地址,执行到arr[12]=0,这时i的值也随之变化,i=0。
又重新进去循环,一直这样下去,因此会死循环。
变量i和数组的地址之间差8个字节(图片上每一个空格代表的是一个整形-->4个字节)
这个差值是随机值吗?这个得看编译器。在gcc编译器中这个差值是4个字节,VS编译器下是8个字节,vc 6.0编译器中这个差值为0个字节。
重复一下原因:
1.局部变量存放在栈区。
2.因为栈区在使用的时候先使用高地址在使用低地址(变量i先使用,在高地址处,数组后使用在变量i的下面)
3.因为数组的内存存储,是连续的,地址随着下标的增长由低到高的变化。
这些原因导致数组越界的时候,会找到i的地址。
4.先创建i,在创建数组也可以算一个原因。如果先创建数组,在创建i就不会发生死循环,只会越界。
解决方法就是,数组不要越界。
2.数组名
数组名是首元素的地址,存在两个例外,其它时候都是指首元素地址。
第一个例外
sizeof()中,大家有发现,在之前我们求数组元素个数的时候就用到了,sizeof(数组名)求的是整个数组在内存中占用多大的内存,单位是字节。
sizeof中数组名单独出现的时候代表是整个数组
第二个例外
&数组名,指的是整个数组的地址。
3.下标
有两个方面
第一个不能越界。越界了,我们编译是不会报错的,写的时候需要注意。
如果写循环的话,推荐用前闭后开的写法
第二个起始下标为0。
二.指针
1.指针的含义
指针是内存中最小单元的编号,每一个编号是一个地址,指针是编号,也是地址。
需要注意下,我们口头上说的指针,指的是指针变量。
#include <stdio.h>
int main()
{
int a = 10;//在内存中开辟一块空间
int *p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。
//a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量
中,p就是一个之指针变量。
r
起始地址存在指针中。
1.1内存地址如何产生的?
什么是内存中最小单元的编号呢?地址又是如何产生的呢?其实是一个问题。
我们电脑中有32根地址线(32位平台),在高电压和低电压的作用下,每根地址线都会产生1或0。
32根地址线由0-1表示
00000000000000000000000000000000
00000000000000000000000000000001
00000000000000000000000000000002
............
10000000000000000000000000000000
10000000000000000000000000000001
..........
11111111111111111111111111111111
产生的这些二进制序列就是最小单元的编号,也是地址。不过编译器下为了方便一般会转化为16进制的。总共有2^32个编号(地址)
每个最小单元是1个字节(8位)
每个地址对应内存空间中一个字节
所以内存大小为2^32Byte,1024的转换下去就是4GB
也有64根地址线的(64位的电脑)
最小单元还是1个字节
能产生2^64Byte内存大小
那指针(地址)的大小是多少呢?
32根地址线,产生的2进制序列有32位--->4个字节
64根地址线,产生的2进制序列有64位--->8个字节
所以指针占4/8个字节,看电脑是32位的还是64位的。
2.野指针
什么是野指针?野指针就是指针指向的位置是不可知的。
野指针的成因
1.指针未初始化。
2.在调用函数的时候,返回值为地址,主函数内指针接收这个地址。这个指针也是野指针。
3.越界访问,指针与数组连用的时候,可能会越界。
第二点:
当我们调用Fun函数的时候,Fun函数才会向内存(栈区)开辟空间,调用完Fun函数,它开辟的空间就会释放回去,里面所创建的变量,同样也会释放回去。
这样的话,arr是可以接收到&a,可以找到它,但这个时候这个地址不在属于我们了,已经还给操作系统了,属于是非法当问。
另一种理解,局部变量的生命周期是在{}内部,在回到主函数后,已经离开了a所在的{},a开辟的空间自然就释放回去了。
3.指针的类型存在的意义
有两个意义:
1.类型决定了指针能操作的内存大小
int 类型能操作4个字节
char类型能操作1个字节
......
2.类型决定了指针加减整数时候的步长
int类型的指针+1向后走4个字节
char类型的指针+1能像后走1个字节
......
两者对比一下,我们就能发现问题。
想深入了解存储方式,可以看看 整形与浮点型的存储
4.指针的运算
4.1指针加减整数
这个就是上面类型的意义中有提到的,加(减)整数向后(前),偏移多少个字节取决与类型。
4.2指针之间的比较
我们就看个例题,逆序字符串
void reverse_string(char* string)
{
char* left = string;
char* right = string + strlen(string) - 1;
while (left <= right)
{
char tmp = 0;
tmp = *left;
*left = *right;
*right = tmp;
left++;
right--;
}
}
int main()
{
char arr[50] = { 0 };
gets(arr);
reverse_string(arr);
printf("%s", arr);
return 0;
}
4.3指针减指针
指针减指针 ,算出来的结果为两指针中间的元素个数。
5.二级指针
int a = 0 ; int* pa =&a 这样的叫一级指针, 二级指针是一级指针的地址,二级指针变量指向一级指针变量。
int* * pb = &pa。 我在写的时候,特意分开了一些, 中间的 * 表示它是个指针, int* 表示指针pb的指向地址所存放内容的类型。
同样也可以通过*改变a的值
6.指针数组
指针数组是指针还是数组呢?是数组,存放指针的数组,也就是存放地址的数组。
简单应用 :二级指针模拟实现二维数组。
三.指针与数组之间的联系
1.操作符之间的转换
这样一讲再去理解二级指针模拟二维数组应该更好理解了。
arr4+i-->arr4[i] 后面的这个arr4[i],是因为arr4本身就是数组。借用数组的写法。
2.注意事项
在使用函数的时候,实参为数组需要注意。
我这样写对吗?不对,为什么呢?
原因很简单,实参是arr,arr是数组名,数组名就是地址,我们讲过了,地址占内存的大小是根据电脑是32位平台还是64位平台。sizeof(arr)-->4 sizeof(arr[0])-->4 -->求的是整形的大小。
怎么解决呢?在主函数中求出数组的大小,在进行传参。
形参写的时候的两种方法,一种就是用数组,另一种是指针。传过来数组,用数组接收没毛病吧。传过来是个地址,用指针接收也没毛病。
总结
小结一下,我们回顾了:
数组的创建初始化,以及存储时候的特点,一些注意事项。《c陷阱与缺陷》的那道题需要多多留意。
指针:指针是什么,指针类型的意义,指针的运算,二级指针,野指针,指针数组,也有一些注意事项。
我们接着回顾了下指针和数组之间的联系。
对内存大体上有所了解,三个大区,栈区,堆区,静态区。复习了,内存是如何编址的,最小的内存单元。
......