偷学C语言第三天

目录

数组

概念

初始化数组

访问数组

变长数组

二维数组

访问二维数组

二维数组的初始化

处理字符串的相关函数

指针

定义指针

小试牛刀

数组和指针

彩蛋


数组

概念

数组(Array)是有序的元素序列。 [1]  若将有限个类型相同的变量的集合命名,那么这个名称为数组名。组成数组的各个变量称为数组的分量,也称为数组的元素,有时也称为下标变量。用于区分数组的各个元素的数字编号称为下标。数组是在程序设计中,为了处理方便, 把具有相同类型的若干元素按有序的形式组织起来的一种形式。这些有序排列的同类数据元素的集合称为数组。

概念解读:数组里面存储的是同类型的数据,它类似于一排连着的房子,在内存中是一块连续的空间。定义数组的语法:数据类型 数组名[存放元素的个数]; ------ 数据类型决定了你要存储什么类型的数据,数组名后面的方括号中用常量或常量表达式指定数组中存放元素的个数,因为你得告诉C编译器你要存多少元素, 它才知道需要向内存申请多大的空间。这就跟你去K歌时定包厢一样,你得告诉服务员你们要来多少个人,服务员才好安排包厢(其实服务员才不管你来多少个人,你一个人开个大包厢人家也不管,还会笑你人傻钱多 >_<)。

初始化数组

在定义数组的同时对其各个元素进行赋值,称之为数组的初始化。初始化方式比较多,看代码:

/*
  演示初始化数组。
*/
#include<stdio.h>
int main() {
	/*
	  方式一:
	  将数组中所有元素初始化为0,其实是把数组第一个元素赋值为0,
	  其他没有赋值的元素会被自动赋值为0。
	*/
	int a[8] = {0};
	/*方式二:如果是赋予不同的值,那么用逗号分隔开即可。*/
	int b[6] = {1, 2, 3, 4, 5, 6};
	/*方式三:表示为前边6个元素赋值,后边3个元素系统自动初始化为0。原理同方式一。*/
	int c[9] = {1, 2, 3, 4, 5, 6};
	/*方式四:如果为所有元素都赋值了,可以不用指定元素个数,编译期会自动判断数组长度*/
	int d[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
	/*方式五:C99新增特性:可以为指定下标的元素赋值,未指定的元素自动赋值为0。*/
	int e[10] = {[2] = 2, [6] = 6, [8] = [8]};

	return 0;
}

访问数组

通过数组下标访问数组:数组名[下标],数组的下标是从零开始!

/*
  演示访问数组。
*/
#include<stdio.h>
/*取数组的长度*/
#define ARRAR_LENGTH(x) sizeof(x)/sizeof(x[0])
int main() {
	int arr[]= {1,2,3,4,5,6};
	for(int i = 0; i < ARRAR_LENGTH(arr); i++) {
		printf("arr[%d] = %d\t",i,arr[i]);
	}
	printf("\n");
	return 0;
}

变长数组

变长数组(VLA,variable length array)是C99 标准新增的特性,这里的变长是指在C代码运行时才去指定数组的长度,但是一旦指定了数组长度,在数组的整个生命周期中就不会再变了。

/*
  演示C99新增特性:变长数组(Variable Length Array --- VLA)
*/
#include <stdio.h>

int main() {
	int n, i;

	printf("请输入字符的个数:");
	scanf("%d", &n);

	char a[n+1];

	printf("请开始输入字符:");
	/*将标准输入流中剩下的 '\n' 扔掉*/
	getchar();
	for (i = 0; i < n; i++) {
		scanf("%c", &a[i]);
	}
	a[n] = '\0';
	printf("你输入的字符串是:%s\n", a);

	return 0;
}

二维数组

定义二维数组与一维数组比较类似,只不过需要两个中空号来指定维度:数据类型 数组名[常量表达式][常量表达式];

	int a[6][6]; // 6*6,6行6列
	char b[6][8]; // 6*8,6行8列
	double c[8][8]; // 8*8,8行8列

 所以多少维数组就用多少个中括号去定义,值得一提的是,几行几列只是一个概念模型,也就是说为了让我们更容易理解才会讲多少行多少列,其实在内存物理模型层面上讲,不过多少维数组都是线性存储的。

从上图我们不难看出,二维数组事实上就是在一维数组的基础上,每个元素存放一个数组。所有多维数组都是以同样的方式实现。

访问二维数组

二维数组的访问方式与一维数组一样,都是通过下标访问,比如:a[6][6]访问的就是第7行第7列的元素,因为数组的下标是从0开始。还有一点要非常小心,就是数组越界,比如一个6行6列的二维数组,你不能访问它第n(n>6)行第n(n>6)列的数据,最可怕的是你即便这么做了,C语言也不会报错,这样一旦因为数组下标越界造成的问题就很难排查。

/*
  演示访问数组时下标越界的情况
*/
#include<stdio.h>
int main() {
	int a[3][2] = {1,2,3,4,5,6};
	printf("a[8][8] = %d\n",a[8][8]);
	return 0;
}

二维数组的初始化

  1. 由于二维数组在内存中是线性存放的,因此可以将所有的元素写在一个花括号内:int a[3][2] = {1,2,3,4,5,6}; C会先初始化第一行元素,再初始化第二行,以此类推;
  2. 为了更直观地表示元素的分布,可以用大括号将每一行的元素括起来,还可以通过换行缩进写成矩阵的样子:
    #include<stdio.h>
    int main() {
    	int a[3][2] = {1,2,3,4,5,6};
    	int b[3][2] = {{1,2},{3,4},{5,6}};
    	int c[3][2] = {
    		{1,2},
    		{3,4},
    		{5,6}
    	};
    
    	return 0;
    }

     

  3.  二维数组也可以仅对部分元素赋初值:int b[3][2] = {{1},{3},{5}}; 像这样写就只会对第一行的元素初始化,其他元素默认被赋值为0;

  4. 如果需要将所有二维数组的元素都赋值为0,可以这么写:int d[2][2] = {0}; 细细品一下,是不是跟一维数组一毛一样,所有温故而知新嘛~

  5. C99同样增加了一种新特性:指定初始化的元素,而未被赋值的元素自动初始化为 0:int e[3][6] = {[0][0] = 1, [1][1] = 2, [2][2] = 3};

  6. 二维数组的初始化也能偷懒,让编译器根据元素的数量计算数组的长度。但只有第 1维的元素个数可以不写,其他维度必须写上:int b[][2] = {{1,2},{3,4},{5,6}}。

处理字符串的相关函数

关于字符串函数的用法都在如下代码示例的注释中,更多C函数库使用方法参见:C 语言标准函数库分类

/*
  演示字符串处理函数,更多C函数库请参见:https://fishc.com.cn/thread-70614-1-1.html
*/
#include<stdio.h>
#include<string.h>
int main() {
	char words[66] = "无边落木萧萧下,不尽长江滚滚来。";

	/*sizeof函数用于计算字符串的尺寸*/
	printf("sizeof = %d\n",sizeof(words));
	/*
	  strlen 函数用于返回指定字符串的长度,中文占字符的个数每个人的环境可能都不一样,我的是占两个字符。
	  一个字符串的长度指的是从起始位置到结束符的字符个数(不包含结束符'\0')。
	*/
	printf("strlen = %d\n",strlen(words));

	/*
	  拷贝字符串案例:
	*/
	char oldString[] = "我爱你";
	char newString[] = "我爱你拥有的一切,包括你拥有的我。";
	/*下面代码会报错,字符串不能通过赋值运算符实现拷贝。*/
	//oldString = newString;
	/*
	  strcpy 函数用于拷贝字符串,包含字符串结束符 '\0'。为了避免溢出,必须确保用于存放的数组长度足以容纳待拷贝的字符串。
	*/
	strcpy(oldString,newString);
	printf("oldString = %s newString = %s\n",oldString,newString);

	/*
	  strncpy与strcpy类似,只是多了一个 n的参数, strncpy(dest,src,n)将拷贝原字符串中的 n个字符到目标字符串中,如果
	  源字符串的长度小于 n,那么就用 '\0'填充额外的空间。如果源字符串的长度大于或等于 n,那么只有 n个字符被拷贝到目
	  标数组中(注意:这样的话将不会以结束符 '\0' 结尾)。 所以我们一般使用 dest[sizeof(dest) - 1] = '\0'; 语句确保
	  目标字符串是以 '\0' 结尾。
	*/
	char oldString2[30] = "";
	char newString2[30] = "Live or die, it is a problem.";
	strncpy(oldString2,newString2,4);
	/**/
	oldString2[4] = '\0';
	printf("oldString2 = %s\n",oldString2);

	/*
	  strcat用于拼接字符串,将源字符串拷贝并连接到目标数组存放的字符串后边,此过程将覆盖第一个参数的结束符 '\0'。
	*/
	char string1[] = "万里悲秋常作客,";
	char string2[] = "老年多病独登台。";
	strcat(string1,string2);
	printf("string1 = %s\n",string1);
	printf("string2 = %s\n",string2);
	/*
	  strncat(dest,src,n)函数用于拷贝源字符串中的 n 个字符到目标数组的字符串后边,并在末尾添加结束符 '\0'。
	*/
	char string3[] = "将相本无种,";
	char string4[] = "男儿当自强。";
	strncat(string3,string4,sizeof(string4));
	printf("string3 = %s\n",string3);
	printf("string4 = %s\n",string4);

	/*
	  strcmp函数用于比较两个字符串。该函数从第一个字符开始,依次比较每个字符的 ASCII码大小,直到发现两个字符不相等或抵达结束符('\0')为止。
	  返回值有三种情况 :
	  小于0 --- 字符串1的字符小于字符串2对应位置的字符;
	  等于0 --- 两个字符串的内容完全一致;
	  大于0 --- 字符串1的字符大于字符串2对应位置的字符。
	*/
	char string5[] = "日有熹,月有光。";
	char string6[] = "富且昌,寿而康。";
	char string7[20] = "日有熹,月有光。";
	if (!strcmp(string5, string6)) {
		printf("string5和string6两个字符串相同!\n");
	} else if(!strcmp(string5, string7)) {
		printf("string5和string7两个字符串相同!\n");
	} else {
		printf("两个字符串不同!\n");
	}

	/*
	  strncmp(dest,src,n)函数用于比较两个字符串的前 n个字符。该函数从第一个字符开始,依次比较每个字符的 ASCII码大小,
	  发现两个字符不相等或抵达结束符('\0')为止,或者前 n 个字符完全一样,也会停止比较。
	*/
	char string8[] = "balabalahahaha。";
	char string9[] = "balabalabiubiubiu。";
	if (!strncmp(string8, string9,8)) {
		printf("string8和string9前8个字符相同!\n");
	} else {
		printf("string8和string9前8个字符不同!\n");
	}

	return 0;
}

指针

哇,该来的总是会来的,大学的时候就被这玩意搞成二傻子了,这次一定要把它拿下!(ง •̀_•́)ง

我们一般认为指针就是地址,由指针变量存放指针。指针变量也有类型,它的类型就是存放的地址指向的数据类型。关于指针的概念参见:指针

指针内存模型

定义指针

其实我们说的定义指针是指定义指针变量,语法格式:数据类型 *指针变量名

可以看出定义指针变量与定义普通变量的唯一区别就是在指针变量的前面多了一个星号“*”。数据类型表示指针变量中存放的地址指向的内存单元的数据类型(有点绕开~),比如上图指针内存模型中,*pa指向了 a,而 a是 int类型,所以这个指针也是 int类型。

小试牛刀

/*
  演示指针的使用
*/
#include<stdio.h>
int main()
{
	int a = 666;
	char b = 'b';
	/* & 在这里叫做取址符,下面这行代码意思是把变量 a的地址取出来,存放在指针变量 pa中。*/
	int *pa = &a;
	char *pb = &b;

	/*通过指针变量间接访问 a和 b,这块的 *是用来获取指针变量指向的变量的值,而非定义指针*/
	printf("a = %d,b = %c\n",*pa,*pb);
	/*通过指针变量访问 a在内存中的地址*/
	printf("a的地址:%p\n",pa);
	return 0;
}

注意:避免访问未初始化的指针!未初始化的指针被形象地称为野指针,C会给它赋值一个随机值,如果这个随机值是不合法的还好一点,至少系统会异常退出,我们改变不了指针的指向;但是如果这个随机值刚好是合法的地址,就会把人家地址原本的值覆盖掉,出现莫名其妙的错误,而且很难排查!

/*
  演示野指针
*/
#include <stdio.h>
int main()
{
	float *a;
	/*没有被初始化的指针叫野指针,因为它的地址是随机的*/
	*a = 123;
	printf("%f\n",*a);
	printf("%p",a);

	return 0;
}

数组和指针

在我们印象中,数组似乎跟指针没什么关系,那么看看下面的例子,可能你就不这么认为了:

/*
  演示指针与数组的关系:
*/
#include <stdio.h>
#include <string.h>
int main()
{
	/*指针变量 str指向一个字符串常量*/
	char *str = "one life one love.";
	int length = strlen(str);

	for(int i = 0; i < length; i++) {
		/*居然可以像数组那样通过下标获取元素的值*/
		printf("%c",str[i]);
	}
	printf("\n");

	/*如果神公然与人作对,那是任何人都难以应付。《荷马史诗》*/
	char saying[] = "If God openly confronts people,it is difficult for anyone to deal with it.";
	int length_saying = strlen(saying);
	for(int i = 0; i < length_saying; i++) {
		/*我们并没有定义指向 saying这个数组的指针,却可以通过指针法访问数组元素。*/
		printf("%c",*(saying+i));
	}
	printf("\n");
	return 0;
}

哇,是不是很神奇?但是数组和指针不是一回事,不然C语言的作者干嘛定义两个概念呢。莫慌,带着这个疑问我们继续往下看~

数组名是数组第一个元素的地址,也是数组的首地址。

/*
  数组名是数组第一个元素的地址,也是数组的首地址。
*/
#include <stdio.h>
int main()
{
	char words[128];
	printf("请输入你想说的话:");
	/*
     注意这里我们的字符串没有用取址符 &,这是因为数组名本身就是一个地址信息,
     它是数组的首地址。
   */
	scanf("%s",words);

	printf("%s\n",words);
	printf("words的地址是:%p\n",words);
	printf("words的地址是:%p\n",&words[0]);

	return 0;
}

由于数组是一种同类元素挨个排列的数据结构,所以下一个元素的地址就是上一个元素的地址加上该类元素所占的内存空间:

/*
  演示指针与数组的关系:
*/
#include <stdio.h>
#include <string.h>
int main()
{
	char motto[] = "大鹏一日同风起,扶摇直上九万里。";
	short s[] = {1,2,3,4,5,6};
	int i[] = {1,2,3,4,5,6,7,8};
	float f[] = {1.1F,2.2F,3.3F,4.4F,5.5F,6.6F};
	double d[] = {1.1,2.2,3.3,4.4,5.5,6.6};

	/*打印上面各个数组前三个元素的地址,可以发现地址间距刚好是元素所占字节数*/
	printf("motto[0] --> %p\t motto[1] --> %p\t motto[2] --> %p\n",&motto[0],&motto[1],&motto[2]);
	printf("s[0] --> %p\t s[1] --> %p\t s[2] --> %p\n",&s[0],&s[1],&s[2]);
	printf("i[0] --> %p\t i[1] --> %p\t i[2] --> %p\n",&i[0],&i[1],&i[2]);
	printf("f[0] --> %p\t f[1] --> %p\t f[2] --> %p\n",&f[0],&f[1],&f[2]);
	printf("d[0] --> %p\t d[1] --> %p\t d[2] --> %p\n",&d[0],&d[1],&d[2]);

	printf("------------------------------------------------分割线------------------------------------------------\n");

	/*
	 通过指针指向数组第一个元素实现指针指向数组,因为这样以来,我们可以通过对指针进行加减运算的方式,
	 指向数组中的每一个元素。指针的加和减运算对应指向距离指针所在位置的向前和向后。
	*/
	char *p_motto = motto;
	short *p_s = s;
	int *p_i = i;
	float *p_f = f;
	double *p_d = d;

	/*通过指针的方式打印上面各个数组前三个元素的地址*/
	printf("motto[0] --> %p\t motto[1] --> %p\t motto[2] --> %p\n",p_motto,p_motto+1,p_motto+2);
	printf("s[0] --> %p\t s[1] --> %p\t s[2] --> %p\n",p_s,p_s+1,p_s+2);
	printf("i[0] --> %p\t i[1] --> %p\t i[2] --> %p\n",p_i,p_i+1,p_i+2);
	printf("f[0] --> %p\t f[1] --> %p\t f[2] --> %p\n",p_f,p_f+1,p_f+2);
	printf("d[0] --> %p\t d[1] --> %p\t d[2] --> %p\n",p_d,p_d+1,p_d+2);

	/*指针法:使用指针间接访问数组元素的方法。*/
	char motto_en[] = "Dapeng rose with the wind in a day,soaring for ninety thousand miles.";
	char *p_motto_en = motto_en;
	printf("motto_en[0] --> %c\t motto_en[1] --> %c\t motto_en[2] --> %c\n",*p_motto_en,*(p_motto_en+1),*(p_motto_en+2));

	return 0;
}

彩蛋

<1>解答上一篇博文的思考题:

#include <stdio.h>
int main()
{
int a = 3;
printf("%d,%d\n",a++,a++); 
printf("\n%d",a);
 
return 0;
}

运行结果:

这里用过关键的知识点要告诉大家,printf函数输出是自右向左编译的,所以最右边的 a先被赋值再自增,输出的就是3,之后a的值就变成了4;接着左边的 a先被赋值为 4,再自增,所以输出是 4,之后 a变成了5,所以最后一行代码的输出结果就是5。

<2>不管数组存放的是什么类型的元素,指针变量加1怎么就能定位到数组的下一个元素?O(∩_∩)O哈哈~ 依然是一个思考题,不然你们怎么会有意犹未尽的感觉,没有意犹未尽的感觉,哪来的学习兴趣对吧︿( ̄︶ ̄)︿

你要学会猥琐发育,不鸣则已,一鸣惊人!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值