C语言入门

零.写在前面

本人使用的是VS2019编译器,本人是某双非大学学生,这个文章是自己学习过程中的一些总结,如有错误,敬请指导!

1.C语言入门

C语言一经出现就以其功能丰富、表达能力强、灵活方便、应用面广等特点迅速在全世界普及和推广。C语言不但执行效率高而且可移植性好,可以用来开发应用软件、驱动、操作系统等。

1.1什么是C语言

C语言是一门通用计算机编程语言,广泛应用于底层开发。C语言的设计目标是提供一种能以简易
的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语
言。
尽管C语言提供了许多低级处理的功能,但仍然保持着良好跨平台的特性,以一个标准规格写出的
C语言程序可在许多电脑平台上进行编译,甚至包含一些嵌入式处理器(单片机或称MCU)以及超
级电脑等作业平台。
二十世纪八十年代,为了避免各开发厂商用的C语言语法产生差异,由美国国家标准局为C语言制
定了一套完整的美国国家标准语法,称为ANSI C,作为C语言最初的标准。 [1] 目前2011年12月8
日,国际标准化组织(ISO)和国际电工委员会(IEC)发布的C11标准是C语言的第三个官方标
准,也是C语言的最新标准,该标准更好的支持了汉字函数名和汉字标识符,一定程度上实现了汉
字编程。
C语言是一门面向过程的计算机编程语言,与C++,Java等面向对象的编程语言有所不同。
其编译器主要有Clang、GCC、WIN-TC、SUBLIME、MSVC、Turbo C等。

1.2第一个C语言程序

//写一个C代码,打印hello bit
#include<stdio.h>// 头文件
int main()//主函数有且仅有一个
{
    printf("hello bit\n");//printf 为打印 \n换行
    return 0;
}

output:hello bit      

1.C程序就是执行主函数里的代码,也可以说这个主函数就是C语言中入口,有且仅只有一个入口就是main函数

2.而main前面的int就是主函数的类型,其中int为整形类型;

3。printf()是格式输出函数,他的功能为打印,其中%d为打印整形,\n是转义字符中的换行符;

4.return是函数的返回值,根据函数类型的不同,返回的值也是不同的。

C程序一定一定是从主函数开始执行的!!!!

1.3写代码的一些小习惯

  1. 一个说明或一个语句占一行,、一个可执行语句结束都需要换行
  2. 函数体内的语句要有明显缩进通常以按一下Tab键为一个缩进
  3. 括号要成对写,如果需要删除的话也要成对删除
  4. 当一句可执行语句结束的时候末尾需要有分号
  5. 代码中所有符号均为英文半角符号
  6. 写注释是给自己和其他程序员去看的,可以用//注释内容/* 注释内容 */进行注释

2.数据的类型与大小

2.1基本数据类型       

                                       

    char           //字符数据类型   
    short          //短整型
    int              //整形
    long          //长整型
    long long  //更长的整形
    float         //单精度浮点数
    double    //双精度浮点数


2.2每种数据类型的大小

#include <stdio.h>
int main()
{
  printf("%d\n", sizeof(char));//printf为打印、\n换行、sizeof(类型)计算大小单位为字节
  printf("%d\n", sizeof(short));
  printf("%d\n", sizeof(int));
  printf("%d\n", sizeof(long));
  printf("%d\n", sizeof(long long));
  printf("%d\n", sizeof(float));
  printf("%d\n", sizeof(double));
  printf("%d\n", sizeof(long double));
  return 0;
}

其中printf为打印、\n换行、sizeof(类型)计算大小单位为字节 ;

output:

2.3各种类型的相互转换:

char类型数据转换为int类型数据遵循ASCII码中的对应值.

其中:

字节小的可以向字节大的自动转换,但字节大的不能向字节小的自动转换

例如:char可以转换为int,int可以转换为double,char可以转换为double。但是不可以反向。

强制类型转换
强制类型转换是通过定义类型转换运算来实现的。其一般形式为:

(类型名) (表达式) 或者 (类型名) 表达式

作用是把表达式的运算结果强制转换成类型说明符所表示的类型

例:

#include <stdio.h>
int main()
{
	int m, n;
	float x, y;
	x = 1.1;
	y = 2.2;
	m = (int)(y - x);
	n = (int)x + m;
	printf("m=%d\nn=%d\nx=%.2f\ny=%.2f\n", m, n, x, y);
	return 0;
}

结果:

      程序中的语句m=(int)(y-x);实现把y-x的结果强制转化为整型,得到m=1,语句n=(int)x+m;中把x的类型强制转化为整型,得到表达式的值为2,上述的转化对x和y的数据类型不会产生影响。

需要注意的点:

  1. 转换后不会改变原数据的类型及变量值,只在本次运算中临时性转换
  2. 强制转换后的运算结果不遵循四舍五入原则。

 2.4总结

  如图:几类主要数据类型所占用的字节数以及应用

  如图:其他数据类型所占用的字节数

输出语句,也可以说是占位输出,是将各种类型的数据按照格式化后的类型及指定的位置从计算机上显示。

其格式为:printf("输出格式符",输出项);

当输出语句中包含普通字符时,可以采用一下格式:

printf("普通字符输出格式符", 输出项);

   目的:存在各种类型的目的是为了更加丰富表达生活中各种类型的值。

3.变量以及常量

在C语言的学习中肯定存在常量以及变量,常量比如性别、身份证号码、手机号码等等。而变量则包含手机号码、身高、体重(顺便说一句我最近瘦了)等等。

3.1定义变量的方法

类型+变量名+赋值号+所赋值的内容

int age =150;
float weight=80.5f;
char  ch='w';

3.2变量的命名

只能由字母(包括大写和小写)、数字和下划线( _ )组成。
不能以数字开头。
长度不能超过63个字符。
变量名中区分大小写的。
变量名不能使用关键字,要保证其命名有实际的意义!

3.3变量的分类与应用

变量总共分为两类一类是局部变量一类是全局变量,其中局部变量在{}内定义使用,而全局变量一般在{}外定义。顾名思义局部变量的生命周期只有{}内一小部分,出了{}后就不可以使用了,而全局变量的生命周期则是整个代码周期。其本质为在内存中开辟一块儿区域,用于数据的存放,当使用完毕后再将空间返还给系统。

下面来看一段代码:

#include <stdio.h>
int global = 2019;//全局变量
int main()
{
  int local = 2018;//局部变量
  //下面定义的global会不会有问题?
  int global = 2020;//局部变量
  printf("global = %d\n", global);
  printf(" local = %d\n", local );
  return 0;
}

可以看到我们定义了两个global的变量一个为全局变量,另一个为局部变量,那么使用的时候会不会存在问题?如果没有问题输出的值是多少?

可以看到global=2020;local=2018;那么可以说明一点即:

当局部变量和全局变量同名的时候,局部变量优先使用!

3.4变量的作用域

定义:作用域(scope)是程序设计概念,通常来说,一段程序代码中所用到的名字并不总是有效/可用的而限定这个名字的可用性的代码范围就是这个名字的作用域。
1. 局部变量的作用域是变量所在的局部范围。
2. 全局变量的作用域是整个工程。
生命周期
变量的生命周期指的是变量的创建到变量的销毁之间的一个时间段
1. 局部变量的生命周期是:进入作用域生命周期开始,出作用域生命周期结束。
2. 全局变量的生命周期是:整个程序的生命周期。

变量作用域是和其生命周期有关的:

#include <stdio.h>
int main()
{
    {
        int a = 100;
        int b = 5;
        int c = a + b;
       printf("%d\n ", c);
    }
    printf("%d\n ", c);//这个c就不会被打印 原因是不在变量的作用域内 即不在{}内
    return 0;
}

output:

可以看到出现了报错!

3.5常量

定义:常量即表述不变的量,例如:比如性别、身份证号码、手机号码等等,在C语言中可理解为             恒定不变的量

3.6常量的分类与应用

1.字面常量

2.const修饰的常量

3.#define定义的标识符常量

4.枚举常量

 3.6.1字面常量

字面常量:在一串代码中直观的数值就叫做字面常量

nt main()
{
  1
  2
  3
  return 0;
}

其中1 2 3就是字面常量,直观的体现了数值的大小;

3.6.2 const修饰的常变量
const float pi=3.14//这里的pi被const进行修饰成了一个常变量
pi=6;//是不能被修改的

如果我们对其进行修改会发生什么事?

看下面一段代码:

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

我们发现这里会报错,其中:指定const对象,这句话意思就是const后面的数已经是常量了,已经是定值,不能更改了,而在后面给定a=10,因此报错。

其实呢,const函数是常属性,令a =1;有常属性,让他有常量的属性,让它从可变成为不变。加上const之后a就不可变了,其实不加const之前a是可变的,加上const函数之后a就成了常变即为常变量。由于他是常变量其本质仍旧为变量因此不可以用来定义数组名的大小,我们用代码说话:

void main
{
const  int a=10;
int arr[a]={0};//由于a是常变量因此此处无法定义数组的大小
}

常变量又是怎么理解呢?我们这里可以理解为:一个中国人,放弃了中国国籍,加入外国国籍,他是外国人,但是他身上仍然流的是中国人的血。const修饰的常变量在C语言中只是在语法层面限制了变量不能改变,但其本质仍旧为一个变量。


3.6.3#define定义的标识符常量

#define 定义的标识符常量:定义完就是常量,可以理解为给常量加了一个名字,我们看看一下一段代码:

#include <stdio.h>
#define SIZE 10
int main()
{
    int a = SIZE;
    int arr[SIZE] = { 0 };
    printf("%d\n",a);
    return 0;
}

OUTPUT:

可以看到输出的值为10,且无任何报错,此时SIZE就是10,已经被完全定义为一个常量了,后续只要使用SIZE它就是10这个常量;因此它可以用来定义数组大小(作者使用的是VS2019的编译器是不可以用变量定义数组大小的)

 3.6.4枚举常量

枚举常量:一一列举;可一一列举的东西如:性别,三原色,血型等等;枚举常量默认从0开始,中途不可以修改但是初始值可以修改的,可以利用枚举类型创建初值并且赋值。其中enum这个函数叫做枚举值,我们用一小段代码演示一下这个东西:


#include <stdio.h>
enum sex
{
    male,
    secret=3,
    female
};

int main()
{
    {
      
        printf("%d\n ", male);
        printf("%d\n ", secret);
        printf("%d\n ", female);
    }
    return 0;
}

上述代码中enum最后括号(})的;是不能少的,这是enum函数的格式,中间的元素与元素之间必须要有逗号隔开,然后我们打出我们的主函数,执行代码看一下所输出的值

OUTPUT:

 

可以看到我们创建了三个枚举值,可以看到male为0,这是因为枚举值默认从0开始,中途不可以修改然而后面两个是3和4,这是由于其初始值可以修改的。

4.字符串+转义字符

4.1字符串

"hello bit.\n"

由双引号(Double Quote)引起来的一串字符称为字符串字面值(String Literal),或者简称字符串。其结束标志为\0,且\0占用一定的空间

我们用一段代码来体现一下这个操作:

#include <stdio.h>
int main()
{
    char ch1 = 'a';
    char ch2[] = { 'a','b','c','\0'};
    char ch3[] = { 'a','b','c','d' };
    char ch4[] = "abcd";
   printf("%c\n ", ch1);
   printf("%s\n ", ch2);
   printf("%s\n ", ch3);
   printf("%s\n ", ch4);//printf以% s打印从给定地址打印,直到遇到\0
    return 0;
}

OUTPUT:

可以看到结果:ch1,ch2,ch4输出正常,但是ch3的输出为乱码这是因为printf以% s打印从给定地址打印,直到遇到\0,但是ch3没有\0,检测不到结束标志,因此打印的为乱码

再看一下下面这段代码:

#include <stdio.h>
#include <string.h>
int main()
{
    char ch1 = 'a';
    char ch2[] = { 'a','b','c','\0' };
    char ch3[] = { 'a','b','c','d' };
    char ch4[] = "abcd";
    //int len1 = strlen(ch1);
    int len1 = strlen(ch2);
    int len2 = strlen(ch3);
    int len3 = strlen(ch4);//stelen打印字符串长度\0不统计在内, 用%d打印
    printf("%d\n", len1);
    printf("%d\n", len2);
    printf("%d\n", len3);
    return 0;
}

其中strlen是库函数,用来求字符串的长度

OUTPUT:

可以看到\0也是占一位的,由于ch3没有结束标志,因此打印不是我们想要的字符的长度

4.2转义字符

我们先看这段代码:

#include <stdio.h>
int main()
{
printf("c:\code\test.c\n");
  return 0;
}

我们的目的是打印出来: c:\code\test.c这个计算机文件的路径,我们看看实际打印出来了什么?

OUTPUT:

哎?可以看到这和我们所期望的并不符合,那么我们应该如何操作呢?我们可以引入转义字符来达成刚才的操作。

转义字符的分类以及含义:

目的:转变字符意思,防止被理解成其他的字符

我们用代码感受一下:

#include <stdio.h>
int main()
{

	printf("are you ok \?\n");//打印?
	printf("c:\\test\\test.c\n");//打印\t
    printf("%c\n","\'");//打印'
	printf("%c\n", "\"");//打印"
	printf("a\\tb");//打印\t
    printf("\a");//警告字符 蜂鸣器
	printf("avc\bvvv\n");//退格符  打印之后退一个格
	
	return 0;
}

OUTPUT:

 可以看到我们用转义字符打印,才可以把这些符号完全的打印出来,不然会被理解成其他意思,导致我们的表述错误。

同时:

//   \? 让?不再是三字母词 不被解析为三字母词中的?,用于打印?
//   \' 用于表示字符常量'即打印单引号
//  \"  用于表示字符常量"即打印双引号
//  \\  用于打印字符/
//  \a 警告字符  警告字符 蜂鸣器
//  \b退格符  打印之后退一个格
// \n换行 \r回车 

// \ddd   ddd表示1-3个八进制数字
// \xdd   其中dd表示2个十六进制数字 

5.几类运算符

5.1运算符的分类

1.算术运算符:加减乘除等

2.赋值运算符:用于简单赋值运算和复合赋值运算

3.关系运算符 :大于小于等于等

4.逻辑运算符:与、或、非

5.三目运算符:判断表达式

5.2算数运算符:

C语言中的算数运算符主要完成以下几种操作:

1.对于除法运算符运算时如果两数均为整数,则得到结果为整数,若两数有一个为小数则得到结果为小数。

2.在求余运算符运算是其结果是与符号有关系的

5.2.1自增运算符以及自减运算符

  自增运算符为++,其功能是使变量的值自增1

  自减运算符为--,其功能是使变量值自减1。

有以下几种用法:

同理a+2、a-2的用法同上

5.3赋值运算符

C语言中赋值运算符分为简单赋值运算符复合赋值运算符

简单赋值运算符=号了,下面讲一下复合赋值运算符:

复合赋值运算符就是在简单赋值符=之前加上其它运算符构成.

例如:+=、-=、*=、/=、%=等等(如果不好理解可以多谢几条完成此类功能)

5.4关系运算符

 关系表达式的值是,在C程序用整数10表示,其中0为假,1为真。

壹、写在前面

下面我们开始语句部分,C语言的语句可以分为以下五类:

1.表达式语句

2.函数调用语句

3.控制语句

4.复合语句

5.空语句

在下面的几个章节中我们重点讲解控制语句:

控制语句是用于控制程序的执行流程的,用于实现程序的各种结构方式(顺序结构、选择结构、循环结构),这三种语句由自己特定的方式进行编译书写,C语言总共有九类控制语句,分别是:

1.选择语句(分支语句):if,switch语句

2.循环执行语句:while,do...while,for语句

3.转向语句:break,goto,continue,return语句

6.选择语句(分支语句)

6.1if语句(不能用break)

1.实现逻辑:

if(表达式)

{

执行语句

}

其语义是:如果表达式的值为真,则执行其后的语句,否则不执行该语句。

注意:if()后面没有分号,直接写{};

或者可以采用if else 的表达形式

if(表达式)

{

执行语句1

}

else

{

执行语句2

}

其语义是:如果表达式的值为真,则执行语句1,否则执行语句2

其实也可以采用if else  else....的表达形式

if(表达式)

{

执行语句1(代码块儿1)

}

else if

{

执行语句2(代码块儿2)

}

.....

else

{

执行语句n(代码块儿3)

}

其语义是:依次判断表达式的值,当出现某个值为真时,则执行对应代码块,否则执行代码块n。

注意:当某一条件为真的时候,则不会向下执行该分支结构的其他语句。

再再再或者也可以if else套嵌使用,这个我们后期再说

我们举个简单的栗子:

如果你好好学习,走向人生巅峰

如果你不好好学习,直接走向人生低谷

我们引用一张其他机构的图片简单说明:

如果用C语言如何实现呢?我们展示一段代码:

#include <stdio.h>
int main()
{
	printf("好好学习吗\?\n");//我们利用了前面的操作符打印了?
	int flag = 0;
	scanf("%d", &flag);
	if (flag == 1)
		printf("发SCI");
	else
		printf("发EI");
	return 0;
}

OUTPUT:

可以看到当我们输入1时候执行第一条语句,若输入其他值的时候执行第二条语句。

6.1.1悬空的else

定义:else是和他最近的if进行匹配的,不管你if—else是如何嵌套使用,对不对齐,你看的顺不顺眼,我else只会和离得最近的if去匹配

OK!我们来看这么一段儿代码吧先:

​​​​​​​# include <stdio.h>
int main()
{
    int a = 0;
    int b = 2;
    if (a == 1)
        if (b == 2)
            printf("haha");
        else
            printf("hehe");
    return 0;
}

我们得到的答案会是hehe或者haha吗?如果你这么想那你肯定错了我们来看看输出的结果吧

OUTPUT:

为什么什么都没打印呢?我们现在就得注意一个小点就是悬空else的问题、

我们都知道,if表达式后只能跟一条子句,若该字句中含有多条语句,则必须用大括号括起来变成一个语句块,这是为了方便编译,同时一个语句快在if内也被认为是一条语句

那么我想问一下:最上面的if(a==1)表达式后的字句是谁呢?在这里千万注意,if(a==1)表达式的子句可不是if(b==2)这个表达式啊,而是:

if (b == 2)
       printf("hehe\n");​​​​​​​

因为if语句其实是一个整体,所以if表达式和后面的那条字句加起来才是一条完整的语句,若仅仅只有if表达式,那是不被语法所认可的,if(a==1)后面的if-else语句理所当然的被认为是一个完整的语句(代码块儿),那么我们是不是就理解了为什么打印为空!

#include<stdio.h>
int main()
{
    int a = 0;
    int b = 2;
    if (a == 1)
        if (b == 2)
            printf("hehe\n");
    else
        printf("haha\n");
    return 0;
}

else只会和离得最近的if去匹配。我们为了避免此类问题,那就一定要养成好的代码风格。如下:

#include<stdio.h>
int main()
{
    int a = 0;
    int b = 2;
    if (a == 1)
    {
        if (b == 2)
        {
            printf("hehe\n");
        }
        else
        {
            printf("haha\n");
        }
    }
    return 0;
}

OUTPUT:

这次就正确了,我们要合理的利用{},以此来合理的划分代码块儿,你在读之前代码时是按照你的代码风格来看待的,所以习惯性的以为这个else是和与之对齐的那个if所匹配,从而导致输出的结果与你所想象的不符合。

6.2switch语句

1.实现形式:

switch(整形表达式)

{

case(常量表达式1):

执行代码块儿1;

break;

case(常量表达式2):

执行代码块儿2;

break;

......

case(常量表达式n):

执行代码块儿n;

break;

default

执行代码块儿n+1

}

需要注意的几个点:

1.在case后的各常量表达式的值不能相同,否则会出现错误。
2.在case子句后如果没有break;会一直往后执行,一直到遇到break,才会跳出switch语句。
3.switch后面的表达式语句只能是整型或者字符类型。
4.在case后,允许有多个语句,可以不用{}括起来。
5.各case和default子句的先后顺序可以变动,而不会影响程序执行结果。
6.default子句可以省略不用。

哦~还有个小点需要注意一下的:如果你并不想忽略不匹配所有标签的表达式的值时该怎么办呢?
可以在语句列表中增加一条default子句
default:
1.写在任何一个 case 标签可以出现的位置。
2.当 switch 表达式的值并不匹配所有 case 标签的值时,这个 default 子句后面的语句就会执行。
所以,每个switch语句中只能出现一条default子句。
3.它可以出现在语句列表的任何位置,而且语句流会像执行一个case标签一样执行default子句。


我们举一个小栗子:

我们的想法是输入1——7打印1——7这几个数字,然后跳出,输入其他时打印输入错误并且跳出。

# include <stdio.h>
int main()
{
	int day = 0;
	scanf("%d", &day);
	switch (day)
	{
	case 1:
		printf("1\n");
		break;
	case 2:
		printf("2\n");
		break;
	case 3:
		printf("3\n");
		break;
	case 4:
		printf("4\n");
		break;
	case 5:
		printf("6\n");
		break;
	case 6:
		printf("6\n");
		break;
	case 7:
		printf("7\n");
		break;
	default:
		printf("输入错误");
		break;

	}
	return 0;
}

OUTPUT:

可以看到我们达成了操作,输入我们的想法是输入1——7,执行case1——case7这几个语句,打印1——7这几个数字,然后跳出;输入其他时执行default,输入错误并且跳出。

6.3插入一个小点关于putchar()和getchar()

putchar():打印字符

getchar():获取字符

# include <stdio.h>
int main()
{
	int ch = 0;
	while ((ch = getchar())!=EOF)//获取字符
	//printf("%c\n",ch);
	putchar(ch);//打印字符
	return 0;
}

OUTPUT:

可以看到我们不仅读取了数据同时也输出了数据

简单分析一下:

1.EOF的本质为 - 1,在函数读取失败的时候为EOF
2.scanf读取成功,返回值为读取到数据的个数
3.读取失败返回EOF    
4.getchar读取成功,返回值为字符的ASC码值
读取失败返回EOF    

7.循环语句

有些事情我们必须一直去做,比我吃饭,比如呼吸比如一日一日的学习,循环主要有以下三个分类分别是:

while循环;

do...while循环;

for循环;

7.1while循环

1.实现形式:

while(条件判断)

{

执行语句

}

while语句的语义是:计算表达式的值,当值为真(非0)时, 执行循环体代码块

需要注意的几个点:

1.while语句中的表达式一般是关系表达或逻辑表达式,当表达式的值为假时不执行,反之一直执行

2.一定要记着在循环体中改变循环变量的值,否则会出现死循环

3.循环体如果包括有一个以上的语句,则必须用{ }括起来,组成复合语句,保证代码的可读性

4.while循环中,当条件表达式成立时,才会执行循环体中语句,每次执行期间,都会对循环因子进行修改(否则就成为死循环),修改完成后如果while条件表达式成立,继续循环,如果不成立,循环结束,因此while条件判断将会比循环体多执行一次

举个小栗子:

我们分析一下下面程序的输出结果吧

#include <stdio.h>
int main()
{
	int a = 0, b = 0;
	for (a = 1, b = 1; a <= 100; a++)
	{
		if (b >= 20) 
         {
           break;
         } 
		if (b % 3 == 1)
		{
			b = b + 3;
			continue;
		}
		b = b-5;
	}
	printf("%d\n", a);
	return 0;
}

OUTPUT:

分析:

第1次循环:a = 1,b=1--->b小于20,if不成立,b%3==1%3==1成立,b=b+3, b的值为4

第2次循环:a = 2,b=4--->b小于20,if不成立,b%3==4%3==1成立,b=b+3, b的值为7

第3次循环:a = 3,b=7--->b小于20,if不成立,b%3==7%3==1成立,b=b+3, b的值为10

第4次循环:a = 4,b=10--->b小于20,if不成立,b%3==10%3==1成立,b=b+3, b的值为13 

第5次循环:a = 5,b=13--->b小于20,if不成立,b%3==13%3==1成立,b=b+3,b的值为16

第6次循环:a = 6,b=16--->b小于20,if不成立,b%3==16%3==1成立,b=b+3, b的值为19

第7次循环:a = 7,b=19--->b小于20,if不成立,b%3==19%3==1成立,b=b+3, b的值为22

第8次循环:a = 8,b=22--->b大于20,if成立,循环break提出

循环结束最后打印a:8

7.1.1while循环之continue的作用是什么?

continue语句的作用是跳过本次循环体中余下尚未执行的语句,立即进行下一次的循环条件判定,可以理解为仅结束本次循环。
注意:continue语句并没有使整个循环终止。

举个小栗子吧:

#include<stdio.h>
int main()
{
	int i = 1;
	while (i<=10)
	{
		if (i == 5)
		{
			++i;
			continue;
		}
		printf("%d ", i);
		++i;
	}
	printf("\n");
	return 0;
}

OUTPUT:

 分析:

当执行到i==5的时候,会进入if语句中,然后continue返回判断条件继续,不再继续执行下面的语句。在这里要注意,需要i++,不然会一直在这里死循环。

7.1.2while循环之break的作用是什么?

break语句是用来停止跳出循环的,直接跳出不执行后续的循环,在没有循环结构的情况下,break不能用在单独的if-else语句中。在多层循环中,一个break语句只跳出当前循环。

好的,我们举一个小栗子:

#include <stdio.h>
int main ()
{
   int i = 1 ;
   while ( i <= 10 )
   {
     if ( i == 5 )
     {
     break ;
     }
   printf ( "%d " , i );
   i = i + 1 ;
   }
return 0 ;
}

理论上来讲这个循环应该是执行至i<=10结束,但是由于i==5时候我们添加了一条break的语句,因此当i=5时,循环即跳出了,我们看一下输出的结果吧

OUTPUT:

​​​​​​​​​​​​​​

 只输出了1 2 3 4没有后续的几个数字,可知break语句是用来停止跳出循环的

7.2do...while循环

1.实现形式:

do

{

执行语句

}while(表达式);//注意这里有分号

do-while循环语句的语义是:

它先执行循环中的执行代码块,然后再判断while中表达式是否为真,如果为真则继续循环;如果为假,则终止循环。因此,do-while循环至少要执行一次循环语句。(先执行后判断);

 使用dowhile结构语句时,while括号后必须有分号。 

好的,让我们举一个小栗子:

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

OUTPUT:

​​​​​​​​​​​​​​

可以看到打印了1——6这么几个数字,因为是先执行后判断,因此当i=5时候先执行了i++而后才执行的判断语句因此最后输出的结果是6(先执行后判断)! ! ! 

7.2.1do...while循环之continue的作用是什么?
#include<stdio.h>
int main()
{
    int i = 0;
    do
    {
        if (5 == i)
            continue;
        printf("%d ", i);
        i++;
    } while (i <= 10);
    return 0;
}

OUTPUT:

可以看到导致死循环了,为什么呢?这是因为当i==5时,执行continue语句,又返回了do位置执行,i==5又执行continue,因此陷入了死循环的状态。

7.2.2do...while循环之break的作用是什么?

break的使用,break功能一直都是跳出循环的作用

 我们举个小栗子:​​​​​​​

#include<stdio.h>
int main()
{
    int i = 0;
    do
    {
        if (5 == i)
            break;
        printf("%d ", i);
        i++;
    } while (i <= 10);
    return 0;
}

OUTPUT:

 其break作用和while循环中break的作用类似,因此不过多做讲解了

7.3for循环

1.实现形式:

for(表达式1;表达式2;表达式3)

{

执行语句

}

它的执行过程如下:

1.执行表达式1,对循环变量做初始化;(例如i=0;)
2.执行表达式2,进行判断若其值为真(非0),则执行for循环体中执行代码块,然后向下执行;若其值为假(0),则结束循环;(例如i<=5;)
3.执行表达式3,对于循环变量进行操作的语句;(例如i++)
4.执行for循环中执行代码块后执行第二步;第一步初始化只会执行一次。
循环结束,程序继续向下执行。
注意:for循环中的两个分号一定要写
其中:

表达式1是一个或多个赋值语句,它用来控制变量的初始值;
表达式2是一个关系表达式,它决定什么时候退出循环;
表达式3是循环变量的步进值,定义控制循环变量每循环一次后按什么方式变化。
 

需要注意的几个点:

1.for循环中的“表达式1、2、3”均可不写为空,两个分号(;;)不能缺省。
2.省略“表达式1(循环变量赋初值)”,表示不对循环变量赋初始值。
3.省略“表达式2(循环条件)”,不做其它处理,循环一直执行(死循环)
4.省略“表达式3(循环变量增减量)”,不做其他处理,循环一直执行(死循环)。
5.表达式1可以是设置循环变量的初值的赋值表达式,也可以是其他表达式。
6.表达式1和表达式3可以是一个简单表达式也可以是多个表达式以逗号分割。

7.表达式2一般是关系表达式或逻辑表达式,但也可是数值表达式或字符表达式,只要其值非零,就执行循环体。各表达式中的变量一定要在for循环之前定义。

举个小栗子吧

#include<stdio.h>
int main()
{
    int i = 0;
   for(i=0;i<=5;i++)
    {
        printf("%d ", i);
    } 
    return 0;
}

OUTPUT:

OK我们再举个例子:

判断一下这个for循环的输出值

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

OUTPUT:

我们按F10 进入调试看看:

 为什么是0?我们按F10进入调试一条条执行后可以发现循环的内容并没有执行为什么?

这是由于我们判断语句中的k=0导致的,在C语言中k=0;其中0为假,判断不会进去循环,直接跳过循环了,输出了0 0 ,这是我们要注意的点 !!!​​​​​​​

 7.3.1for循环之continue的作用是什么?

对于for循环而言​​​​​​​continue语句的作用是跳过本次循环体中余下尚未执行的语句,立即进行下一次的循环条件判定,可以理解为仅结束本次循环。
注意:continue语句并没有使整个循环终止。

举个例子吧:​​​​​​​

#include<stdio.h>
int main()
{
    int i = 0;
   for(i=0;i<10;i++)
    {
       if (i<5)
       {
           continue;
       }
       printf("%d ", i);
    }
    return 0;
}

OUTPUT: 

具体continue的判断方式可以参考之前while循环判断方式

 7.3.2for循环之break的作用是什么?

break的使用,break功能一直都是跳出循环的作用,这里就不过多举例了,和上面的一致

7.4go to语句

C语言中也有这样的语句,就是goto语句,goto语句是一种无条件分支语句.

1.实现形式:

goto 语句标号;
一般不建议使用,因此不过多讲解了

7.5部分练习

学习了这么久的循环,我们简单的做些联系,用几道例题巩固一下成果

7.5.1n的阶乘计算

我们直接上代码:

#include <stdio.h>
int main()
{
	int i = 0;
	int a = 0;
	int b = 1;
	scanf("%d", &a);
	for (i = 1; i <=a; i++)
	{
		b = b * i;	
	}
	    printf("%d\n", b);
		return 0;
	}

OUTPUT:

n的阶乘就是1*2*3*.....*n,那么我们就可以用for循环遍历整个n数字,让第二个数字乘第一个然后一次类推直到第n个。

 7.5.2计算1!+2!+....n!

话不多说上代码:

#include <stdio.h>
int main()
{
	int n = 0;
	int i = 0;
    int  sum=0;
	int ret = 1;
	int a = 0;
    printf("请输入数字:");
	scanf("%d",&a);
	for (n = 1; n <=a; n++)
	{
		 ret = 1;
//ret放在此处用于外层循环保证ret的初始值恒定为1
//这样计算下面for循环中的每个数字的阶乘时候就是从1开始的
     for (i=1;i<=n;i++)
      {
		ret = ret * i;	
       }
	 sum += ret;
	}
	    printf("阶乘为:%d",sum);
		return 0;
	}

OUTPUT:

我们这样写是延续了上面计算n阶乘的思路,先计算每个数字的阶乘,保存到sum内,而后一步一步累加起来,我们可以直接算出来每个数字的阶乘直接累加起来,看代码:

#include <stdio.h>
int main()
{
	int n = 0;
	int i = 0;
	int  sum = 0;
	int ret = 1;
    int a = 0;
    printf("请输入数字:");
	scanf("%d",&a);
	for (n = 1; n <= a; n++)
	{
		ret = ret * n;
		sum += ret;
	}
	printf("阶乘为:%d",sum);
	return 0;
}

得到的结果和上面是一致的 ,这样子就简化了一部分代码

7.5.3有序数组寻找某个数字N(二分法)

具体二分法的计算可以看一下这篇文章

直接看我们的代码:

#include <stdio.h>
int main()
{
	int arr[] = { 0,2,3,4,5,6,7,8,9,10};
	//printf("%d\n", sizeof(arr));//sizeof计算数组的大小,单位为字节
	//printf("%d\n", sizeof(arr)/ sizeof(arr[0]));//sizeof计算数组的大小,单位为字节
	int k = 1;
	int i = 0;
	int flag = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
		int left = 0;
		int right = sz - 1;
		while(left<=right)
		{ 
		int mid = (left + right) / 2;
		if (arr[mid] == k)
		{
			printf("找到了");
			flag = 1;
			break;
		}
		else if (arr[mid] < k)
		{
			left = mid + 1;
		}
		else
		{
			right = mid - 1;
		}
		if (flag == 0)
			printf("找不到");
		   break;
		}
	return 0;
}

OUTPUT:

贰 写在前面

在下面的章节中我们主要介绍一下函数:

8.函数

1.函数的定义:在计算机中,子程序(函数)是一个大型程序中的某部分的代码,有一个或者多个语句块儿组成的,相对于其他代码有一定的独立性。

8.1函数的分类

库函数:把一些常用的功能封装起来,方便我们使用

OK,那么就产生了一个问题,为什么有库函数?

早期的c语言是没有函数的,他只规定你语法,只要你语法合适就可以进行操作,但是有一天程序员A写一个功能用的A方式,B写了一个打印程序用的B方式,C也写了一个打印程序用的C方式,同一个功能在每个人的手上写的可能都不同,这就导致同一个功能代码的复杂性,因此出现库函数方便我们队一些常用功能的规范化、标准化。那这个时候呢,就有了库函数的概念,这个函数只要把参数规定死了。 返回类型规定死了。函数名规定死了,那他的使用方法肯定就是一模一样了,库函数的出现让我们代码的其实开发效率的变高了。 代码的更加标准化了。

注:但是库函数的使用必须包含(include)对应的头文件
 

我们可以在下面这个网站学习C语言的库函数

cplusplus.com - The C++ Resources Network

常用的几个库函数有:

●IO函数:输入输出函数

●字符串操作函数:strlen、strcmp等

●字符操作函数:islower、issupper等

●内存操作函数:memset、memcmp等

●时间日期操作函数:time等

●数学函数

如果库函数什么都干了还需要程序员干嘛?哎哟你干嘛呀对吧,因此引入自定义函数的概念

自定义函数:顾名思义自定义函数就是程序员自己设计的函数,有函数名、返回值类型、已经相关参数都需要我们自己进行操作。

1.实现形式:

ret_type  fun_name(para1, * )
{
statement;//语句项
}

其中:
ret_type 返回类型
fun_name 函数名
para1   函数参数

我们举个小栗子:

我们把这个例子延伸一下:

#include <stdio.h>
int get_max(int x, int y)
{
return (x>y)?(x):(y);//如果x大于y就返回x要不返回y
}

int main()
{
int num1 = 10;
int num2 = 20;
int max = get_max(num1, num2);
printf("max = %d\n", max);
return 0;
}

 这个程序的意义是寻找两个数字里面的最大值,朋友们可以自己运行一下,看看结果。

操作方式为使用main函数对get_max函数的调用,在get_max中实现我们的功能  

OUTPUT:

可以看到输出的结果是10和20中的极大值

好的,我们再看第二个例子:

#include <stdio.h>
void Swap1(int x, int y)
{
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}


void Swap2(int *px, int *py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}

int main()
{
int num1 = 1;
int num2 = 2;
Swap1(num1, num2);
printf("Swap1::num1 = %d num2 = %d\n", num1, num2);
Swap2(&num1, &num2);
printf("Swap2::num1 = %d num2 = %d\n", num1, num2);
return 0;
}

OUTPUT:

 我们分析一下这两个函数,他们的功能都是交换内容,即输入1,2输出2,1。两个函数的不同之处在于从主函数传递的内容不同,Swap1直接将值传递给了函数体,而Swap2将两个数字的地址传递给了函数体,我们可以看到Swap2达成了我们的操作,而Swap1产生了问题,这是为什么呢?这就引入了传值调用(实参)以及传址调用(形参)。我们简单分析一下这个东西!!!

8.2实际参数

1,定义:真实传给函数的参数,叫实参。实参可以是:常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。

2.使用规则:对于实际参数,当实参传递给形参时候,形参是实参的一份拷贝,在地址开辟了一片新的地方用于拷贝,因此所以对形参的修改不更改实参。

8.3形式参数

1.定义:形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。

2.使用规则:使用形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。

8.4传值调用

对于Swap1我们打开调试窗口可以看一下:

分析:我们可以看到a,b把值传递给函数中的x,y时,在函数内又开辟了一块儿新地址,进行存储,函数操作完之后,保存在新的地址了并没有把值传递回来,因此并没有进行交换,这里可以看到 Swap1 函数在调用的时候, x , y 拥有自己的空间,同时拥有了和实参一模一样的内容。

这就是传值调用:我们可以简单的认为:形参实例化之后其实相当于实参的一份临时拷贝。其中形参和实参分别占有不同的内存块儿,对形参的修改不影响实参。

8.5传址调用

那么对于Swap2呢,我们也可以看一下:

分析:我们可以看到&a,&b把值传递给函数中的x,y时,在函数内并没有开辟一块儿新地址,进行存储,函数操作完之后,仍旧使用的原地址,把参数传递回了原地址,因此产生进交换,达成我们的预期效果。实参中,我将a,b的地址给传了进去,形参又用pa和pb两个指针存储了a,b的地址,接着用 * 操作符找到了a,b的地址并修改了里面的内容。

利用的就是传址调用:传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。通过指针访问外部变量,改变外部变量时候会影响内部的参数。

8.6函数的嵌套调用以及链式访问

8.6.1函数的套嵌调用:
#include <stdio.h>
void new_line()
{
printf("hehe\n");
}
void three_line()
{
  int i = 0;
for(i=0; i<3; i++)
 {
    new_line();
 }
}
int main()
{
three_line();
return 0;
}

我们可以看到主函数调用了three_line()函数,而three_line()函数又调用了new_line(),这就是函数的套嵌调用,需要注意的是函数可以套嵌调用,但是不可以套嵌定义,因为每个函数是相互独立的存在。

8.6.2函数的链式访问

#include <stdio.h>
int main()
{
  printf("%d", printf("%d ", printf("%d ", 43)));
  //printf函数的返回值是打印在屏幕上字符的个数
  return 0;
}

OUTPUT:

链式访问把一个参数的返回值当做参数访问,我们可以理解为

 把第一个printf的值43的返回值A当做参数让第二个printf打印,然后吧把第二个参数的返回值当做参数B让第三个printf打印。

那么问啥结果是43  4  2??我们来看看,其中计算返回值的时候空格儿也会占一位,我们打印43的时候后面又两个空格,因此其返回值A为4那么打印出来是4也就不奇怪了

 8.7函数的声明

 目的:

 分模块儿写代码,完成不同类型工作

程序在执行的时候是一步一步往下走的,我们调用一个函数时,要有函数的声明,可如果函数写在了main函数的上方,则不需要声明了,如果函数在其他文件里那么是需要声明的,但是函数的定义是可以在程序的任何位置的,即使不在程序内也可以,但是使用的时候必须声明!

 8.8函数定义

函数的定义是交代函数的具体功能的

8.9函数的定义与声明的使用方法

 举个例子比如我们现在要写一个加法的计算:

1.主函数:

 2.函数体功能实现(定义)在另一个文件里

3.函数头文件在另另一个文件里

※※※※※三个不同文件,包含的三个内容,一般头文件盒主函数是一个,实现功能的函数可以有很多个

那么为什么要把源文件和头文件分开呢?

具体可以看这篇文章为什么分开写头文件和函数

8.10函数的递归

程序调用自身的编程技巧称为递归( recursion)。递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的主要思考方式在于:把大事化小

递归:

1.函数自己调用自己

2.递归:也就是先递推而后回归,先递后归※把推过去的值计算后回归

8.10.1递归的两个必要条件 

存在限制条件,当满足这个限制条件的时候,递归便不再继续。
每次递归调用之后越来越接近这个限制条件

举个例子说明一下:

我们输入一个数字然后打印他的个位十位百位千位,假设我们输入的1234

#include<stdio.h>
#include<string.h>
#include<math.h>
void print(int n)
{
	if (n > 9)
	{
		print(n / 10);
	}
	printf("%d ",n%10);
}


int main()
{
	int num = 0;
	scanf("%d", &num);
	print(num);
	return 0;
}

OUTPUT:

原理讲解:

红色的为递推过去的,蓝色为返回来的,旁边数字为执行顺序,同理

 有一些好的例子也可以看这篇博客:由浅入深的理解递归

9.数组                  

9.1一维数组的初始化以及创建

数组的定义:数字是一组相同类型的集合

 type_t  arr_name  [const_n];
//type_t 是指数组的元素类型
//const_n 是一个常量表达式,用来指定数组的大小

 //代码1
int arr1[10];
//代码2
int count = 10;
int arr2[count];//数组时候可以正常创建?
//代码3
char arr3[10];
float arr4[1];
double arr5[20];

在VS2019的编译环境下,代码2无法正常创建,这是因为数组创建,在C99标准之前, [] 中要给一个常量才可以,不能使用变量。在C99标准支持了变长数组的概念,数组的大小可以使用变量指定,但是数组不能初始化。

举一个小栗子:

 9.2一维数组的初始化

1.定义:数组的初始化是指,在创建数组的同时给数组的内容一些合理初始值(初始化)。

我们直接上代码看看结果就知道啦:

#include <stdio.h>
#include <math.h>
#include <string.h>
int main()
{
	int i = 0;
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };//完全初始化
	int arr2[10] = { 1,2,3 };//不完全初始化,剩余默认元素全部为0
	int arr3[10] = { 0 };//不完全初始化为0,剩余默认元素全部为0
	int arr4[] = { 0 };//省略数组大小,数组大小是根据初始化的内容来定的

	printf("arr[10]:\n ");
	for (i = 0; i < 10; i++)
	{
		printf("%d ",arr[i]);
	}
	printf("\n");
	printf("arr2[10]:\n");
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr2[i]);
	}
	printf("\n ");
	printf("arr3[10]:\n");
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr3[i]);
	}
	
	return 0;
}

OUTPUT:

我们一条一条看,们按F10开始调试打开监视窗口与输入数组名:

1.完全初始化时:

int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };

2.不完全初始化时:

 int arr2[10] = { 1,2,3 };

3. 完全初始化为0:

int arr3[10] = { 0 };

4. 省略数组大小:

 int arr4[] = {1,2,3};

 5.超出数组大小存储:

int arr5[3]={1,2,3,4};

 

我们可以看到他已经报错了,因此数组内容的数量必须小于等于其设置的大小

6.不指定字符数组大小

char arr5[] = {'a','b','c'};

7.数组的初始化两类对比:
注意数组大小和最后一项的差别
(1)字符数组存储字符串

char arr6[] = "abcdef";


我们可以看到是按照97,98,99.....的形式存贮的这是因为存储字符是存的是他们的ASC码值
初始化后各元素的值
(2)字符数组的大小和字符串字符个数一致时

char arr6[6] = "abcdef";


这样初始化是有问题的,因为无法正常读取字符串的结束标志('\0'),导致字符串的长度和内容不能得知!

8.字符数组大小大于字符串中的字符数
 

 char arr7[6] = "zxc";

 分析一下:

    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };//完全初始化
  
 int arr2[10] = { 1,2,3 };//不完全初始化,剩余默认元素全部为0
    int arr3[10] = { 0 };//完全初始化为0,剩余默认元素全部为0
    int arr4[] = { 0 };//省略数组大小,数组必须初始化,数组大小是根据初始化的内容来定的

   int arr5[3]={1,2,3,4}//由于数组就三个空间但是存了四个数字那么会报错

我们给出几个结论:

1.数组是具有相同类型的集合,数组的大小(即所占字节数)由元素个数乘以单个元素的大小。
2.数组只能够整体初始化,不能被整体赋值。只能使用循环从第一个逐个遍历赋值。
3.初始化时,数组的维度或元素个数可忽略 ,会根据括号中元素个数初始化数组元素的个数。
4.当括号中用于初始化值的个数不足数组元素大小时,数组剩下的元素依次用0初始化。
5.字符型数组在计算机内部用的时对应的ASC码值进行存储的。
6.一般用”“引起的字符串,不用数组保存时,一般都被直接编译到字符常量区,并且不可被修改。

9.3一维数组的使用

数组的使用我们之前介绍了一个操作符: [] ,下标引用操作符。它其实就数组访问的操作符。

我们直接看代码分析:

#include <stdio.h>
#include <math.h>
#include <string.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	//                0 1 2 3 4 5 6 7 8 9   
 //数组的下标是从零开始的,通过下标进行查找的
	int i = 0;
	int sz = sizeof(arr1) / sizeof(arr1[0]);
	for (i = 0; i < sz-1; i++)
	{
		arr1[i] = i;
	}
		for (i = 0; i < sz; i++)
		{
			printf("%d ", arr[i]);//访问元素大小时候可以使用变量
		}

	return 0;
}

我们来分析一下吧:

1.sizeof()操作符用于取长度,以字节为单位。sizeof(数组名)计算的就是整个数组的大小sizeof(首元素)求的是数组内单个元素的大小。不仅可以用0下标,也可以用其他的下标,用0的原因是数组内至少有一个元素,这样的话不容易出错。那么我们用数组的总长度(总字节数)除以一个元素的长度(字节数)是不是就可以得到数组的元素个数了,如果计算数组的个数可以用sizeof(arr)/sizeof(arr[0])对其进行计算。

2.我们在访问的时候是根据其下标进行访问的,arr[0]访问的第一个数字,arr[1]访问第二个数字,因此我们利用for循环遍历整个数组,对数组进行访问或其他操作。

9.4一维数组在内存中的存储

废话少说,直接看代码吧:

#include <stdio.h>
#include <math.h>
#include <string.h>
int main()
{
	int arr[10] = { 1,2,3,4,5 };
	int sz = sizeof(arr) / sizeof(arr[0]);
 //求数组的长度大小 为10 计算数组元素的方法
 sizeof(arr)求的是数组的总大小  sizeof(arr[0])求的是第一个元素的大小
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("&arr[%d]=%p\n", i, &arr[i]);
	}
	return 0;
}

我们用%p打印每个数组的地址,看看有什么规律

OUTPUT:

哎,我们发现一维数组在内存中是连续存放点,并且是由低地址到高地址进行存放的。

我们发现每次存储都是差四个字节,这是由于一个整形占四个字节,不管他有没有存储够四个字节的东西,他一定也是要占四个字节的空间,因此每次差四,那如果是char类型或者其他类型呢?那肯定就是一个char类型或者让其他类型的长度啦!

这样吧我们给一个小的结论:,我们知道,随着数组下标的增长,元素的地址,也在有规律的递增。由此可以得出结论:数组在内存中是连续存放的!

9.5二维数组的初始化

同理一维数组,二维数组也需要初始化:

我们看一下二维数组的排列方式:其中col为列,row为行

例:

int arr[3][4] = { {1,2,3,4},{5,6,7,8},{9,10,11,12}};

 我们可以看到这是一个三行四列的二维数组,可以理解成一个三行四列的矩阵,排列形式如上图

所示,但是和矩阵不同的是,数组访问的时候的行列是从第0行第0列开始的,这个和一维数组的类似的。

#include <stdio.h>
#include <math.h>
#include <string.h>
int main()
{
	int arr[4][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7},{5,6,7,8,9} };
	int arr2[4][5] = { 1,2,3,4,5,2,3,4,5,6,3,4,5,6,7,4,5,6,7};//满了自动换行
	int arr3[4][5] = { {1,2,3 }, { 4, 5, 2 }, { 3, 4, 5, 6, }};
	//二维数组初始化行可以省略但是列不能省略
	int arr4[][5] = { {1,2,3 }, { 4, 5, 2 }, { 3, 4, 5, 6, }, { 4, 5, 6 } };
    //int arr5[][5] = { {1,2,3 }, { 4, 5, 2 }, { 3, 4, 5, 6, }, { 4, 5, 6 } };
	int j = 0;
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		int j = 0;
		for (j = 0; j < 5; j++)
			printf("%d", arr[i][j]);
		printf("\n");
	}
	printf("\n");
	printf("\n");
	for (i = 0; i < 4; i++)
	{
		int j = 0;
		for (j = 0; j < 5; j++)
			printf("%d", arr2[i][j]);
		printf("\n");
	}
	printf("\n");
	printf("\n");
	for (i = 0; i < 4; i++)
	{
	
		for (j = 0; j < 5; j++)
			printf("%d", arr3[i][j]);
		printf("\n");
	}
	printf("\n");
	printf("\n");
	for (i = 0; i < 4; i++)
	{
		for (j = 0; j < 5; j++)
			printf("%d", arr4[i][j]);
		printf("\n");
	}
	return 0;
}

OUTPUT:

因为二维数组因此我们用两个for循环遍历整个数组进行访问,具体怎么访问后续再给各位细说,现在知道我们把这几个二维数组打印出来就好!

我们来一个一个分析:

1.完全初始化

int arr[4][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7},{5,6,7,8,9} };

对于arr[4][5]这个二维数组,是一个四行五列的数组,第一行存储12345五个数字,第二行存储23456五个数字,后面以此类推。我们可以看到对于这个数组每一行每一列都存满了,如果没存满呢?会是什么情况,看我们接下来的分析。

2.完全初始化另一种形式:

int arr2[4][5] = { 1,2,3,4,5,2,3,4,5,6,3,4,5,6,7,4,5,6,7};

对于arr2[4][5]这个二维数组,没有{}围起来,那么是不是就随便存储呢,我们可以看到输出结果,第一行存储12345,第二行存储23456,也就是说一行存满了自动切换下一行,并不会

因为有没有{}导致存储的失败,但是这样写二维数组显得杂乱还是第一种相对较好。

3.不完全初始化:

int arr3[4][5] = { {1,2,3 }, { 4, 5, 2 }, { 3, 4, 5, 6, }};

对于arr3[4][5]这个二维数组,应该是总共四行,每一行五个数字,但是我们看到前三行均不是五个数字,并且没有第四行,但是依然得到了结果,说明二维数组和一维数组一样,在不完全初始化时,对于没有定义的依旧是补0;

4.不完全初始化另一个形式:

int arr4[][5] = { {1,2,3 }, { 4, 5, 2 }, { 3, 4, 5, 6, }, { 4, 5, 6 } };

可以看出来对于这个二维数组,省略行的定义时候是可以正常运行的,但是省略列的时候就不行了,因此省略行的大小二维数组初始化行可以省略但是列不能省略;

小总结:括号中的一个括号代表一个一维数组的初始化。当里面花括号分组时,按照顺序从第一个开始逐个进行初始化。余下的未赋值的元素用0初始化并且省略行的大小二维数组初始化行可以省略但是列不能省略;

实际上二维数组的本质就是多个一维数组的组合!!!!!!

9.6二维数组的使用

二维数组是采用双重循环套嵌进行使用的一般是用for循环好理解一点点

#include <stdio.h>
#include <math.h>
#include <string.h>
int main()
{
	int arr[4][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7},{5,6,7,8,9} };
	int arr2[4][5] = { 1,2,3,4,5,2,3,4,5,6,3,4,5,6,7,4,5,6,7};//满了自动换行
	int arr3[4][5] = { {1,2,3 }, { 4, 5, 2 }, { 3, 4, 5, 6, }};
	//二维数组初始化行可以省略但是列不能省略
	int arr4[][5] = { {1,2,3 }, { 4, 5, 2 }, { 3, 4, 5, 6, }, { 4, 5, 6 } };
    //int arr5[][5] = { {1,2,3 }, { 4, 5, 2 }, { 3, 4, 5, 6, }, { 4, 5, 6 } };
	int j = 0;
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		int j = 0;
		for (j = 0; j < 5; j++)
			printf("%d", arr[i][j]);
		printf("\n");
	}
	printf("\n");
	printf("\n");
	for (i = 0; i < 4; i++)
	{
		int j = 0;
		for (j = 0; j < 5; j++)
			printf("%d", arr2[i][j]);
		printf("\n");
	}
	printf("\n");
	printf("\n");
	for (i = 0; i < 4; i++)
	{
	
		for (j = 0; j < 5; j++)
			printf("%d", arr3[i][j]);
		printf("\n");
	}
	printf("\n");
	printf("\n");
	for (i = 0; i < 4; i++)
	{
		for (j = 0; j < 5; j++)
			printf("%d", arr4[i][j]);
		printf("\n");
	}
	return 0;
}

OUTPUT:

实际上二维数组的本质就是多个一维数组的组合!!!!!!二维数组中的元素是通过使用下标(即数组的行索引和列索引)来访问的。

其中:多维数组基本的定义是以数组作为元素构成的数组,二维数组的数组元素是一维数组,三维数组的数组元素是一个二维数组,以此类推。也就是说,多维数组用的是一个嵌套的定义!!!!!

9.7二维数组的存储

#include <stdio.h>
int main()
{
	int arr[3][4];
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 4; j++)
		{
			printf("&arr[%d][%d] = %p\n", i, j, &arr[i][j]);
		}
	}
	return 0;
}

OUTPUT:

我们发现二维数组每个元素也是跳过四个字节的,通过结果我们可以分析到,其实二维数组在内存中也是连续存储的。

9.8数组越界

防止数组越界,禁止越界访问!!最好自己做好越界检查!!!!,C语言本身是不做数组下标的越界检查的,编译器不报错不代表程序一定没有问题。举个小栗子说明一下:

#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
  int i = 0;
  for(i=0; i<=10; i++)
 {
    printf("%d\n", arr[i]);//当i等于10的时候,越界访问了
 }
return 0;
}

OUTPUT:

可以看到当i为10的时候就访问越界了,越界后访问的数据就有问题,因此我们要避免数组越界的产生。

9.9数组名的一些奇妙内容

数组名通常是数组首个元素的地址:

1.sizeof(数组名)计算的是整个数组的大小;
 2.sizeof &(数组名),这里的数组名也表示整个数组,取出的是整个数组的地址;

9.9.1 数组作为函数参数
  1. 调用函数传参数组时,减少函数传数组时的成本问题(时间和空间)。因为传参时,需要临时拷贝,如果数组过大,可能会浪费资源,严重的话可能栈溢出。
  2. 数组元素降维成指向数组内部元素类型指针
  3. 对指针加一,加上所指向的类型的大小。

在这里我们引用一下别的朋友的例子:

#include<stdio.h>

void Lisa(int arr[])
{
        printf("a = %d\n", sizeof(arr));
        //数组降维成指针后的指针大小,在32位系统下指针都为4字节
        printf("b = %d\n", sizeof(arr[0]));//数组首元素的大小
        printf("sz =a / b  = %d\n", sizeof(arr) / sizeof(arr[0]));//大小为1
        printf("arr = %p\n", arr);//数组首元素地址
        printf("&arr = %p\n", &arr);//指针的地址
        printf("arr + 1 = %p\n", arr + 1);//下一个元素的地址
        printf("&arr + 1 = %p\n", &arr + 1);//指针下一项的地址
}

int main(void)
{
        int Shuzu[10] = { 0,1,2,3,4,5,6,7,8,9 };
        printf("a = %d\n", sizeof(Shuzu));//数组总大小
        printf("b = %d\n", sizeof(Shuzu[0]));//数组首元素大小
        printf("sz =a / b = %d\n", sizeof(Shuzu) / sizeof(Shuzu[0]));//数组元素个数
        printf("Shuzu = %p\n", Shuzu);//数组首元素地址
        printf("&Shuzu = %p\n", &Shuzu);//代表整个数组,但是地址仍是首元素地址
        printf("Shuzu + 1 = %p\n", Shuzu + 1);//下一个元素的地址
        printf("&Shuzu + 1 = %p\n", &Shuzu + 1);//跳过整个数组后紧挨着的地址
        //此时该地址减去首元素地址等于数组大小
        printf("\n\n");
        Lisa(Shuzu);

        return 0;
}

OUTPUT:

这样我们再分析一下数组与地址的一些内容:


#includ<stdio.h>
 
int main()
{
    int arr[5] = {1,2,3,4,5}
    return 0;
}

看这段代码:

如上创建了一个简单的整型数组a,对于这行代码我们需要明确以下两个点:

       · arr - 数组名 - 也是数组首元素的地址

       · arr因为是首元素的地址,是一个常量,所以不可以赋值或者自增自减

       但是arr+=1这种形式会报错的

地址的大小:如果在x32的机器上,有32bit用来存储一个内容,而8bit为1字节所以地址的大小为4个字节,x64位机器上同理为8个字节。

实例1讲解:
        printf("%d",sizeof(arr));  sizeof(数组名),此时sizeof计算的是整个数组的大小,那么上边的结果就很容易得到了:arr是一个整型数组,这里计算的是整个数组的大小,一个整形的大小为4个字节,整个数组的大小则为4*5=20

实例2讲解: 

        printf("%d",sizeof(arr+0));arr是数组的首元素地址,arr+0就是将该地址向后移动0个 int* 指针单位,也就是还是首元素地址,到那时不同的是,这里的内容不再是数组名了而是一个单纯的地址,地址的大小为4(x32)或8(x64)。

 实例3讲解:

        printf("%d",sizeof(&arr));小补充:&数组名 - 取出的是整个数组的地址,在值上与首元素地址相同,但是当进行加减运算时,其变化的长度是不同的,arr+1是首元素地址+1,指向第二个元素,但是&arr+1则是跳过整个数组,增加一个数组大小的长度,直接指向了数组最后一个元素后的位置。 但是 sizeof() 只会计算传入数据的大小即使是整个数组的地址,它还是一个地址,地址是不分高低贵贱的,只要是地址,其大小都是4或8。

实例4讲解:

        printf("%d",sizeof(*arr)); arr是首元素地址,首元素地址解引用拿到首元素也就是第一个元素,计算第一个元素大小,因为是整形数组,整形的大小为4个字节。

  实例5讲解:     

        printf("%d",sizeof(arr+1)); arr是首元素地址,首元素地址加一为第二个元素的地址,其依旧为地址,所以大小为4或8个字节。

  实例6讲解: 

        printf("%d",sizeof(arr[1]));明确arr[1]的含义:以arr为起始地址向后移动1个单位(4个字节,因为是int数组),然后将它的内容拿出来,并且计算内容的大小,(这里可以把 [] 理解为一次解引用,在后边分析二维数组的时候会容易很多),所以起始就是计算数组中下标为1的元素的大小,因为是整形数组,所以大小为4。

OK我们再分析一组吧:

#include<stdio.h>
int main()
{
	char arr[] = "abcdef";//其中末尾有\0也是要占一个空间的
	printf("arr=%d\n", sizeof(arr));
	printf("arr+0=%d\n", sizeof(arr + 0));
	printf("*arr=%d\n", sizeof(*arr));
	printf("arr[1]=%d\n", sizeof(arr[1]));
	printf("&arr=%d\n", sizeof(&arr));
	printf("&arr+1=%d\n", sizeof(&arr + 1));
	printf("&arr[0]+1=%d\n", sizeof(&arr[0] + 1));
	return 0;
}

 OUTPUT:

实例7讲解:

        printf("arr=%d\n", sizeof(arr));这里计算的是整个数组的大小,结果就很容易得到了,arr是一个字符型数组,这里计算的是数组的大小,需要注意的是\0也是要占一位大小的,因此整个数组的大小为1*7=7;

实例8讲解:

        printf("arr+0=%d\n", sizeof(arr + 0));arr是首元素的地址,arr+0就是这个地址向后移动了0个char*的地址,但是还是首元素的地址,大小为4/8;

实例9讲解:

        printf("*arr=%d\n", sizeof(*arr)); arr是首元素地址,首元素地址解引用拿到首元素也就是第一个元素,计算第一个元素大小,因为是字符型数组,字符型的大小为1个字节。

实例10讲解:

        printf("arr[1]=%d\n", sizeof(arr[1]));以arr为起始地址向后移动1个单位(1个字节,因为是char数组),然后将它的内容拿出来,并且计算内容的大小,(这里可以把 [] 理解为一次解引用,在后边分析二维数组的时候会容易很多),所以起始就是计算数组中下标为1的元素的大小,因为是字符型数组,所以大小为1。

实例11讲解:

        printf("&arr=%d\n", sizeof(&arr));&数组名 - 取出的是整个数组的地址,在值上与首元素地址相同,但是当进行加减运算时,其变化的长度是不同的,arr+1是首元素地址+1,指向第二个元素,但是&arr+1则是跳过整个数组,增加一个数组大小的长度,直接指向了数组最后一个元素后的位置。 但是 sizeof() 只会计算传入数据的大小即使是整个数组的地址,它还是一个地址,地址是不分高低贵贱的,只要是地址,其大小都是4或8。

实例12讲解:

        printf("&arr+1=%d\n", sizeof(&arr + 1));arr是首元素地址,首元素地址加一为第二个元素的地址,其依旧为地址,所以大小为4或8个字节。

实例13讲解:
          printf("&arr[0]+1=%d\n", sizeof(&arr[0] + 1));  以arr为起始地址向后移动1个单位(1个字节,因为是char数组),然后将它的内容拿出来,并且计算内容的大小,(这里可以把 [] 理解为一次解引用,在后边分析二维数组的时候会容易很多),所以起始就是计算数组中下标为1的元素的大小,因为是字符型数组,所以大小为1。

OKOKOK,我们再分析一组二维数组的例子:

前面我们分析了二维数组的存储形式:

对于下面这个二维数组即arr[0][0]一直到arr[2][3]全部为0 ,我们往下继续看:

#include<stdio.h>
int main()
{
	char arr[3][4] = { 0 };
	printf("arr=%d\n", sizeof(arr));
	printf("arr[0][0]=%d\n", sizeof(arr[0][0]));
	printf("arr[0]=%d\n", sizeof(arr[0]));
	printf("arr[0]+1=%d\n", sizeof(arr[0]+1));
	printf("*(arr[0] + 1)=%d\n", sizeof(*(arr[0] + 1)));
	printf("arr+1=%d\n", sizeof(arr + 1));
	printf("*arr+1=%d\n", sizeof(*(arr+1)));
	printf("&arr+1=%d\n", sizeof(&arr + 1));
	printf("&arr[0]+1=%d\n", sizeof(&arr[0] + 1));
	printf("*arr+1=%d\n", sizeof(*arr));
	printf("arr[3]=%d\n", sizeof(arr[3]));
	return 0;
}

OUTPUT:

我们一组一组的进行分析:

实例14讲解:
        printf("arr=%d\n", sizeof(arr));此时sizeof计算的是整个数组的大小,那么上边的结果就很容易得到了:arr是一个整型数组,这里计算的是整个数组的大小,一个整形的大小为4个字节,整个数组的大小则为3*4*1=12(行*列*元素大小)

实例15讲解:

        printf("arr[0][0]=%d\n", sizeof(arr[0][0]));以arr为起始地址(1个字节,因为是char数组),然后将它的内容拿出来,并且计算内容的大小,所以起始就是计算数组中下标为[0][0]的元素的大小,因为是字符型数组,所以大小为1。

实例16讲解:

        printf("arr[0]=%d\n", sizeof(arr[0]));对于这个arr[0]它是单独放在sizeof内部的并且arr[0]是第一行这个一维数组的名字,因此计算的是第一行的大小!!!!大小为4*1=4

实例17讲解:

        printf("arr[0]+1=%d\n", sizeof(arr[0]+1));,对于这个arr[0]它并没有单独放在sizeof内部,因此arr[0]+1指的是第一行第二个元素的地址,其大小为4/8!!

实例18讲解:

        printf("*(arr[0] + 1)=%d\n", sizeof(*(arr[0] + 1)));arr[0]+1代表的是第一行第二个元素的地址,其中取地址后代表的就是第一行第二个元素的大小即为1。

实例19讲解:

        printf("arr+1=%d\n", sizeof(arr + 1));arr代表首行元素的地址,arr+1代表第二行的地址,大小为4/8.

实例20讲解: 

        printf("*arr+1=%d\n", sizeof(*(arr+1)));前面说可以把*符号看[],那么*(a+1)可以看为a[1],也就指的是第二行元素的大小4*1=4。

实例21讲解:

        printf("&arr[0]+1=%d\n", sizeof(&arr[0] + 1));,前面说可以把*符号看[],那么对于这个就是&*arr+1,&和*抵消就成了arr+1,也就代表是arr+1,表述的就是第二行的地址大小为4/8。

实例22讲解:

        printf("*arr+1=%d\n", sizeof(*arr));对于*arr代表的就是arr,也就是取出计算整个第一行的大小,大小为4*1=4;

实例23讲解:

        printf("arr[3]=%d\n", sizeof(arr[3]));前四个元素大小,4*1=4;

10.指针

10.1指针的定义

1.指针是内存中最小单元的编号,也就是地址,我们平常说的指针就是指的是指针变量,是用来存放内存地址的变量。其中每个内存都有一个唯一的编号,这个编号也被称为地址编号 == 地址 == 指针。地址就是指针!!!

OK!我们来举一个例子理解一下这个东西:

#include<stdio.h>
#include<string.h>
int main()
{
	int a = 10;//内存中开辟一块儿空间
    int	*pa = &a;//pa是专门用来存放地址(指针)的,其中pa就是指针变量
    *pa=100;//此时pa就是a
	printf("%d\n",a);
	return 0;
}

OUTPUT:

其中int a在内存中开辟了一块空间,此时a=10,而pa前面带了个*也就说明*pa是专门用来存放指针(地址)的,其中pa是指针变量,而*就是说明它是指针,int说明pa是一个指针指向的是int类型的变量。这里的*pa的操作就是通过存放的pa的地址找到a并且修改a,因为此时pa和a的地址是一致的。         

换个思路理解一下这段代码:

10.2指针变量的大小

#include<stdio.h>
#include<string.h>
//指针的大小在32位平台是4个字节32个比特位,在64位平台是8个字节64个bit位
void main()
{
	printf("%d\n", sizeof(char*));
	printf("%d\n", sizeof(int*));
	printf("%d\n", sizeof(float*));
	printf("%d\n", sizeof(short*));
	printf("%d\n", sizeof(long*));
	printf("%d\n", sizeof(double*));
}

OUTPUT:

10.3指针变量的一些使用规则

指针使用时主要有取地址运算符&和解引用符*这两个:

1.取地址运算符&:单目运算符&是用来取操作对象的地址。例:&i 为取变量 i 的地址。对于常量表达式、寄存器变量不能取地址(因为它们存储在存储器中,没有地址)。​​​​​​​


2.指针运算符*(间接寻址符):与&为逆运算,作用是通过操作对象的地址,获取存储的内容。例:x = &i,x 为 i 的地址,*x 则为通过 i 的地址,获取 i 的内容。

3. x = *&x,因为&和*是相反的我们可以理解为俩相互抵消
我们举一个小栗子:

//声明了一个普通变量 a
int a;
//声明一个指针变量,指向变量 a 的地址
int *pa;
//通过取地址符&,获取 a 的地址,赋值给指针变量
pa = &a;
//通过间接寻址符,获取指针指向的内容
printf("%d", *pa);
//打印

 4.int*解引用访问4个字节、char*解引用访问1个字节其中指针的类型可以决定指针解引用访问的个数,例如:char*(1个字节)、int*(4个字节)、short*(2个字节)等。同时不同类型的指针是为了存放不同类型变量的地址,例如char*类型的指针是为了存放char*类型变量的地址,其他的以此类推。

5.type*p(*说明了p为指针  type说明了指针的类型)

1.type说明了p指向的对象类型

2.p解引用(*)访问是访问的大小为sizeof(type)

3.其中type可以是int、char、long等

6.指针类型的意义是决定其+1操作访问的步长,不同类型指针操作的步长不同

举个小栗子:

#include<stdio.h>
#include<string.h>
int main()
{
	int a = 1
	int* pa = &a;
	char* pc = &a;
	printf("%p\n",pa);
	printf("%p\n", pa+1);
	printf("%p\n", pc);
	printf("%p\n", pc+1);
	return 0;
}

OUTPUT:

10.4野指针的问题

定义:野指针就是指针指向和位置不可知的(随机的、不正确的、没有明确限制的)

问题1:指针未初始化

#include<stdio.h>
#include<string.h>
int main()
{
	int* pa;//指针变量,局部变量不初始化的时候内容为随机值
	*pa = 20;
	printf("%d\n", *pa);
	return 0;
}

OUTPUT:报错

问题2指针越界访问:

#include<stdio.h>
#include<string.h>
int main()
{
	int arr[10] = { 0 };
	int i = 0;
	int* p = arr;
	for (i = 0;i < 13; i++)
		//当指针范围指向的范围超出数组范围时候,p就是野指针
	{
		*(p++)=i ;
		printf("%d ", arr[i]);
	}
	
	return 0;
}

OUTPUT:BAOCUO 

问题3指针指向的空间释放:

#include<stdio.h>
#include<string.h>
int test()   
//a的空间进入函数创建,出函数还给操作系统,但是访问是非法的
{
	int a = 10;
	return &a;
}

int main()
{
	int* p = test();
	printf("%d\n",*p);
	return 0;
}

分析:当我们调用函数test,调用结束后,返回了一个int*的类型,也就是将a的地址返回到调用函数的地方,即赋给了p,值得注意的是,a这个变量是我们在调用函数时,临时创建的一个变量,函数调用完毕,a之前用的那一块儿内存就会被销毁,所谓销毁就是将相应的空间还给了操作系统。

那么我们如何避免野指针这种现象?
 1.指针初始化,明确指针初始化谁的地址,就直接初始化

2.不知道指针初始化为什么时候,暂时初始化为空指针,NULL
3.小心指针越界
4.指针指向的空间释放,及时置空
5.避免返回局部变量地址
6.使用之前检查其有效性

10.5指针的运算

(1)赋值运算

指针变量可以互相赋值,也可以赋值某个变量的地址,或者赋值一个具体的地址

int *px, *py, *pz, x = 10;
//赋予某个变量的地址
px = &x;
//相互赋值
py = px;
//赋值具体的地址
pz = 4000;

 (2)指针与整数的加减运算

  1. 指针变量的自增自减运算。指针加 1 或减 1 运算,表示指针向前或向后移动一个单元(不同类型的指针,单元长度不同)。这个在数组中非常常用。
  2. 指针变量加上或减去一个整形数。和第一条类似,具体加几就是向前移动几个单元,减几就是向后移动几个单元。
//定义三个变量,假设它们地址为连续的,分别为 4000、4004、4008
int x, y, z;

//定义一个指针,指向 x
int *px = &x;

//利用指针变量 px 加减整数,分别输出 x、y、z
printf("x = %d", *px);		//因为 px 指向 x,所以*px = x

//px + 1,表示,向前移动一个单元(从 4000 到 4004)
//这里要先(px + 1),再*(px + 1)获取内容,因为单目运算符“*”优先级高于双目运算符“+”
printf("y = %d", *(px + 1));		
printf("z = %d", *(px + 2));

(3)关系运算

假设有指针变量 px、py。

px > py 表示 px 指向的存储地址是否大于 py 指向的地址
px == py 表示 px 和 py 是否指向同一个存储单元
px == 0 和 px != 0 表示 px 是否为空指针

//定义一个数组,数组中相邻元素地址间隔一个单元
int num[2] = {1, 3};

//将数组中第一个元素地址和第二个元素的地址赋值给 px、py
int *px = &num[0], *py = &num[1];
int *pz = &num[0];
int *pn;

//则 py > px
if(py > px){
	printf("py 指向的存储地址大于 px 所指向的存储地址");
}

//pz 和 px 都指向 num[0]
if(pz == px){
	printf("px 和 pz 指向同一个地址");
}

//pn 没有初始化
if(pn == NULL || pn == 0){
	printf("pn 是一个空指针");
}

(4)

10.6指针数组和数组数组指针

我们首先来分析一下这两个东西的区别或者说这两个东西分别是什么:

“数组指针”和“指针数组”,我们在数组和指针中间加一个的字,数组的指针:我们知道它是一个指针,他是干嘛的嘞?是一个指向数组的指针。指针的数组:我们知道它是一个数组,它是干嘛的?它是装着指针的数组。因此指针数组是数组,而数组指针是指针。

       需要明确一个优先级顺序:()>[]>*,即圆括号()的优先级大于方括号[]大于解引用*所以:

        (*p)[n]:根据优先级,先看括号内,则p是一个指针,这个指针指向一个一维数组,数组长度为n,这是“数组的指针”,即数组指针;

      *p[n]:根据优先级,先看[],则p是一个数组,再结合*,这个数组的元素是指针类型,共n个元素,这是“指针的数组”,即指针数组。

根据上面两个分析,可以看出,p是什么,则词组的中心词就是什么,即数组“指针”和指针“数组”。

此处引用:指针数组和数组指针

10.6.1指针数组的定义和使用 (*p[n])

指针数组:指针数组是指一个数组里面装着指针,其本质为一个数组;

定义:int *p1[10],由于[]的优先级大于*,所以 p1 先与“[]”结合,可以认为是int*(p1[10])构成一个数组,其中数组名为p1,而“int*”修饰的是数组的内容,即数组的每个元素。也就是说,该数组包含 5 个指向 int 类型数据的指针,如图 1 所示,因此,它是一个指针数组。

我们看一段代码来分析分析它是如何使用的:

#include<stdio.h>
int main()
{
int a = 16;
int b = 932; 
int c = 100; 
int *arr[3] ={ &a, &b, &c};
int **parr = arr; 
printf("%d,%d,%d\n", *arr[0], *arr[1], *arr[2]);
printf("%d,%d,%d\n", *(parr[0]), *(parr[1]), *(parr[2]));
return 0;
}

OUTPUT:

分析:

其中int*arr[3]是指针数组,其中内容有a,b,c;parr是指向数组arr的指针,指向的位置为arr的第0个元素的指针,可以把它认为是int*(*parr)这种形式,括号内的*表述parr是一个指针,括号外面的int*表述parr的指向数据的类型为int*类型。由于arr第0个元素的类型为int*,所以定义parr时要加上俩*或者一个*加[];

10.6.2指针数组的定义和使用( (*p)[n])

定义:对于语句“int(*p)[5]”,“()”的优先级比“[]”高,“*”号和 p2 构成一个指针的定义,指针变量名为 p2,而 int 修饰的是数组的内容,即数组的每个元素。也就是说,p2 是一个指针,它指向一个包含 5 个 int 类型数据的数组,如图 2 所示。很显然,它是一个数组指针,数组在这里并没有名字,是个匿名数组。

  我们看一段代码分析一下:

#include<stdio.h>
int main()
{
	char arr[5] = { 'A','B','C','D','E' };
	char(*p)[5] = &arr;
	printf("%c %c %c",**p,*(*p+1), *(*p + 2));
	return 0;
}

OUTPUT:

分析:

arr代表第一个元素的地址,也就是&arr[0],数组名代表首元素的地址,&arr代表整个数组的地址,arr+1代表第二个元素的地址,&arr+1代表向后移动五个字节的地址。因为p=&arr,*p为第一个元素的地址,**p就取出了第一个元素,后面同理。

10.7二级指针

定义:指针变量作为一个变量也有自己的存储地址,而指向指针变量的存储地址就被称为指针的指针,即二级指针。依次叠加,就形成了多级指针。我们先看看二级指针,它们关系如下:

举一个小栗子:

#include<stdio.h>
#include<string.h>
int main()
{
	int a = 10;
	int* p = &a;
	//p是一级指针变量,指针变量也是变量,变量在内存中也要开辟空间
	int** pp = &p;
	**pp = 1000;
		//二级指针,用于接受存放一级指针变量的地址
		printf("%d\n", a);
	return 0;
}

此处为一个例子:是直接搬运其他朋友的:

int p; //这是一个普通的整型变量


int *p; //首先从P 处开始,先与*结合,所以说明P 是一个指针,然后再与int 结合,说明指针所指向的内容的类型为int 型.所以P是一个返回整型数据的指针


int p[3]; //首先从P 处开始,先与[]结合,说明P 是一个数组,然后与int 结合,说明数组里的元素是整型的,所以P 是一个由整型数据组成的数组


int *p[3]; //首先从P 处开始,先与[]结合,因为其优先级比*高,所以P 是一个数组,然后再与*结合,说明数组里的元素是指针类型,然后再与int 结合,说明指针所指向的内容的类型是整型的,所以P 是一个由返回整型数据的指针所组成的数组


int (*p)[3]; //首先从P 处开始,先与*结合,说明P 是一个指针然后再与[]结合(与"()"这步可以忽略,只是为了改变优先级),说明指针所指向的内容是一个数组,然后再与int 结合,说明数组里的元素是整型的.所以P 是一个指向由整型数据组成的数组的指针


int **p; //首先从P 开始,先与*结合,说是P 是一个指针,然后再与*结合,说明指针所指向的元素是指针,然后再与int 结合,说明该指针所指向的元素是整型数据.由于二级指针以及更高级的指针极少用在复杂的类型中,所以后面更复杂的类型我们就不考虑多级指针了,最多只考虑一级指针.


int p(int); //从P 处起,先与()结合,说明P 是一个函数,然后进入()里分析,说明该函数有一个整型变量的参数,然后再与外面的int 结合,说明函数的返回值是一个整型数据


Int (*p)(int); //从P 处开始,先与指针结合,说明P 是一个指针,然后与()结合,说明指针指向的是一个函数,然后再与()里的int 结合,说明函数有一个int 型的参数,再与最外层的int 结合,说明函数的返回类型是整型,所以P 是一个指向有一个整型参数且返回类型为整型的函数的指针


int *(*p(int))[3]; //可以先跳过,不看这个类型,过于复杂从P 开始,先与()结合,说明P 是一个函数,然后进入()里面,与int 结合,说明函数有一个整型变量参数,然后再与外面的*结合,说明函数返回的是一个指针,然后到最外面一层,先与[]结合,说明返回的指针指向的是一个数组,然后再与*结合,说明数组里的元素是指针,然后再与int 结合,说明指针指向的内容是整型数据.所以P 是一个参数为一个整数据且返回一个指向由整型指针变量组成的数组的指针变量的函数.

  • 8
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值