C语言的基本语法入门

前言

对于大部分程序员,C语言是学习编程的第一门语言,很少有不了解C的程序员。

C语言除了能让你了解编程的相关概念,带你走进编程的大门,还能让你明白程序的运行原理,比如,计算机的各个部件是如何交互的,程序在内存中是一种怎样的状态,操作系统和用户程序之间有着怎样的“爱恨情仇”,这些底层知识决定了你的发展高度,也决定了你的职业生涯。

如果你希望成为出类拔萃的人才,而不仅仅是码农,这么这些知识就是不可逾越的。也只有学习C语言,才能更好地了解它们。有了足够的基础,以后学习其他语言,会触类旁通,很快上手。

C语言概念少,词汇少,包含了基本的编程元素,后来的很多语言(C++、Java等)都参考了C语言,说C语言是现代编程语言的开山鼻祖毫不夸张,它改变了编程世界。

正是由于C语言的简单,对初学者来说,学习成本小,时间短,结合本教程,能够快速掌握编程技术。

所以在成为计算机领域大神前的第一步就是学习C语言的基本语法知识。

C语言IDE (集成开发环境)的推荐

俗话说的好,工欲善其事,必先利其器,想要学习C语言,我们要先选择一个好的编译器,那么下面介绍几个在比较主流的IDE。

Dev-C++

适合特别喜欢VC6的老古董,界面土,但是胜在安装简单,可以通过插件进行代码调试和补全。是我们学院的机房必备的IDE。

Dev C++ 5.11 简体中文版下载地址:
官方下载:https://sourceforge.net/projects/orwelldevcpp/

Visual Studio

Visual Studio 是微软开发的一款 IDE,广泛用于 Windows 平台。Visual Studio 可以根据用户需要,选择和安装多个语言的编译环境,比如C++、C#、VB。正因为如此,其安装包一般都比较大,安装时间也会比较长。Visual Studio 很多操作都是图形化页面,易于理解。安装完毕后即可开始新建项目并进行编码。

这个只要在电脑自带的微软商城中搜索即可。

划重点:

  • 开发功能十分齐全
  • 可安装多种编程语言,例如 C++、C#、VB 等
  • 操作页面图形化,易于理解
  • 安装包较大,下载和安装时间长
  • 社区版免费

VSCode

VSCode 严格来说并不完全是 IDE,它是微软推出的一款可扩展的轻量级编辑器。也就是说,使用 VSCode 开发 C 语言时,用户还得额外下载和安装本地编译器(GC/VC++/Clang)并配置环境变量后,才能编译运行代码。

有兴趣动手的下伙伴可以查看官网进行操作(https://code.visualstudio.com/docs/cpp/config-mingw)。

划重点:

  • 操作页面简洁
  • 基本功能齐全(包括语法高亮、括号匹配、自动补全等)
  • 轻量化,安装包和安装时间较短
  • 可根据自身需求安装第三方插件
  • 需要进一步设置才能编译运行C语言代码
  • 免费使用
  • 但是C++配置十分复杂

CLion(最推荐)

CLion 是 JetBrains 旗下的一款跨平台 C/C++ IDE 开发工具。这款开发工具提供智能编辑器、自动代码重构、代码分析、评估表达式等多种功能。同时,CLion 还支持 GCC、clang、MinGW、Cygwin 编译器以及 GDB 调试器。使用CLion开发C语言,开发者需要下载和安装本地编译器,并配置环境变量。CLion免费试用30天后需要付费,每月需要支付$8.9,但是我们是学生可以通过学生认证进行免费使用。

官网:https://www.jetbrains.com/clion/

学生认证链接:https://www.jetbrains.com/zh-cn/community/education/

学生认证教程:https://blog.csdn.net/qq_36667170/article/details/79905198

划重点:

  • 功能丰富
  • 支持多种编译器
  • 安装和下载相对 VS 较轻量
  • 需要配置环境变量
  • 试用期免费,试用结束后需每月付费
  • 但是日后学习前端、后端、测试都要使用他们家的ide,推荐尽早熟悉

第一个C语言程序

我们将会使用下面的程序输出第一个hello world!

Ciallo~(∠・ω< )⌒☆

#include <stdio.h>//引用头文件,使我们可以使用C语言库本身就已经提供给我们的函数,即库函数
int main()//main()是主函数
{
    printf("hello world\n");//printf()是输出函数,'\n'是换行的意思
    return 0;//使程序退出,0的意思是程序正常退出
}
//解释:
//main函数是程序的入口
//一个工程中main函数有且仅有一个

数据类型

现实世界中存在着很多的数据,例如1、2、3....这样的整数,1.1、2.2、3.3.....这样的小数,还有a、b、c....这样的字符,我们学习编程语言便是为了解决显示问题,于是我们就会使用不同的数据类型对于整数、小数、字符等进行表示。

char        //字符数据类型

short       //短整型

int         //整形

long        //长整型

long long   //更长的整形

float       //单精度浮点数

double      //双精度浮点数
  • 问:那么C语言有没有字符串(多个字符组成的,例如:'abc'这样的)类型呢?

  • 答:C语言本身并没有字符串类型,我们在C语言程序中使用的字符串实际上是字符数组,即多个字符构成的就是字符串!₍ᐢ..ᐢ₎♡

  • 分享两种C语言字符串的初始化:

#include<stdio.h>
int main()
{
    char string1[] = "abc";
    char string2[] = { 'a','b','c' };
    return 0;
}
  • 问:我们为什么需要这么多的数据类型呢?

  • 答:一方面是能够存储更加多样的数据,便于进行数据处理;另一方面的原因就是为了能够更好节约我们的内存空间!至于为什么会这么说呢,后面关于不同数据类型所占的内存空间大小的时候大家能够了解到,并且对其能够有一个更加深入的了解。

  • 那么我们探究一下不同的数据类型的大小૮꒰ ˶• ༝ •˶꒱ა

    #include 
    int main()
    {
      //后面是各个语句的输出结果
      printf("%d\n", sizeof(char));//1
      printf("%d\n", sizeof(short));//2
      printf("%d\n", sizeof(int));//4
      printf("%d\n", sizeof(long));//4
      printf("%d\n", sizeof(long long));//8
      printf("%d\n", sizeof(float));//4
      printf("%d\n", sizeof(double));//8
      printf("%d\n", sizeof(long double));//8
      return 0;
    }
    类型 存储大小 值范围
    char 1 字节 -128 到 127 或 0 到 255
    unsigned char 1 字节 0 到 255
    signed char 1 字节 -128 到 127
    int 2 或 4 字节 -32,768 到 32,767 或 -2,147,483,648 到 2,147,483,647
    unsigned int 2 或 4 字节 0 到 65,535 或 0 到 4,294,967,295
    short 2 字节 -32,768 到 32,767
    unsigned short 2 字节 0 到 65,535
    long 4 字节 -2,147,483,648 到 2,147,483,647
    unsigned long 4 字节 0 到 4,294,967,295
  • 对数据类型的简单类型展示

    int a = 1;
    float b = 1.1;
    char c = 'a';
    long d = 1000000;

变量与常量

生活中的有些值是不变的(比如:圆周率,性别,身份证号码,血型等等)

有些值是可变的(比如:年龄,体重,薪资)。

不变的值,C语言中用常量的概念来表示,变的值C语言中用变量来表示。

定义变量

 int a = 1;
 float b = 1.1;
 char c = 'a';
 long d = 1000000;

变量分类

在C语言中变量也分为两种:

  1. 全局变量

  2. 局部变量

    #include 
    int global = 2023;//全局变量
    int main()
    {
       int local = 2025;//局部变量
       //下面定义的global会不会有问题?
       int global = 2024;//局部变量
       printf("global = %d\n", global);//输出结果为glabal = 2024
       return 0;
    }

总结:

上面的局部变量global变量的定义其实没有什么问题的!

当局部变量和全局变量同名的时候,局部变量优先使用,这就是我们常说的局部优先原则!꒰ঌ( ⌯' '⌯)໒꒱

变量的使用

#include <stdio.h>
int main()
{
    int num1 = 0;
    int num2 = 0;
    int sum = 0;
    printf("输入两个操作数:>");
    scanf("%d %d", &num1, &num2);
    sum = num1 + num2;
    printf("sum = %d\n", sum);
    return 0;
}

此处简单带大家了解一下printf()函数和scanf()所代表的输入输出函数!

一些变量的输入和输出:

long long int  %lld
float          %f
double         %lf//输出可以使用%f
int            %d
long int       %ld
char           %c

c语言输入的本质,其实是将信息写入到已经分配好的地址中。

#include<stdio.h>
int main() {
    int a;
    int b;
    printf("%p\n",&a);
    printf("%p\n",&b);
    scanf("%d",&a);
    scanf("%d",&b);
    printf("%p\n",&a);
    printf("%p\n",&b);
    return 0;
}

这段代码中会显示a,b的地址。

scanf()&printf()

#include <stdio.h>
int main()
{
    int testInteger = 5;
    float f;
    printf("Number = %d", testInteger);
    scanf("%f",&f);//输入浮点数
    printf("Value = %f", f);//输出浮点数  %c输出字符 %o八进制输出 %x 十六进制输出
    return 0;
}//格式化输出各种类型的

输出多位数和指定的长度

#include<stdio.h>
int main()
{
    double a=1.0;
    printf("%20.15f\n",a/3);//这里所有的数占用20个格,保留15位小数
    return 0;
}

getchar

//getchar()的输入,输入一个字符,我们经常将他用于吸收回车
#include <stdio.h>
int main( )
{
   int c;

   printf( "Enter a value :");
   c = getchar( );

   printf( "\nYou entered: ");
   putchar( c );
   printf( "\n");
   return 0;
}

gets() & puts() 函数

char *gets(char *s) 函数从 stdin 读取一行到 s 所指向的缓冲区,直到一个终止符或 EOF。

int puts(const char *s) 函数把字符串 s 和一个尾随的换行符写入到 stdout

#include <stdio.h>

int main( )
{
    char str[100];
    printf( "Enter a value :");
    gets( str );//输入字符串
    printf( "\nYou entered: ");
    puts( str );//输出字符串
    return 0;
}

变量的作用域和生命周期

  1. 作用域

    作用域(scope)是程序设计概念,通常来说,一段程序代码中所用到的名字并不总是有效/可用 的,而限定这个名字的可用性的代码范围就是这个名字的作用域。

    • 1. 局部变量的作用域是变量所在的局部范围。
    • 2. 全局变量的作用域是整个工程
  2. 生命周期

​ 变量的生命周期指的是变量的创建到变量的销毁之间的一个时间段

    1. 局部变量的生命周期是:进入作用域生命周期开始,出作用域生命周期结束。
    1. 全局变量的生命周期是:整个程序的生命周期。

常量

C语言中的常量和变量的定义的形式有所差异。

C语言中的常量分为以下以下几种:

  • 字面常量

  • const 修饰的常变量

  • #define 定义的标识符常量

  • 枚举常量

    简单实例:

    #include 
    //举例
    enum Sex
    {
      MALE,
      FEMALE,
      SECRET
    };
    //括号中的MALE,FEMALE,SECRET是枚举常量
    int main()
    {
      //字面常量演示
      3.14;//字面常量
      1000;//字面常量
    
      //const 修饰的常变量
      const float pai = 3.14f;   //这里的pai是const修饰的常变量
      pai = 5.14;//是不能直接修改的!会报警告!!!
    
      //#define的标识符常量 演示
      #define MAX 100//这个一般写在全局的位置
      printf("max = %d\n", MAX);
    
      //枚举常量演示
      printf("%d\n", MALE);
      printf("%d\n", FEMALE);
      printf("%d\n", SECRET);
      //注:枚举常量的默认是从0开始,依次向下递增1的
      return 0;
    }

运算符

算数运算符

若a=10,b=20,则:

运算符 描述 实例
+ 把两个操作数相加 a + b 将得到 30
- 从第一个操作数中减去第二个操作数 a - b 将得到 -10
* 把两个操作数相乘 a* b 将得到 200
/ 分子除以分母 b/ a 将得到 2
% 取模运算符,整除后的余数 b % a 将得到 0
++ 自增运算符,整数值增加 1 a++ 将得到 11
-- 自减运算符,整数值减少 1 a-- 将得到 9

问:a++和++a有什么区别?

答:a++:后缀自增运算符(Post-increment)

  • a++用在表达式中时,表达式的值是a自增前的值。

  • 自增操作会在表达式的其余部分计算完成后才执行。

    int a = 5;
    int b = a++; // b 被赋值为 5,然后 a 变为 6

++a:前缀自增运算符(Pre-increment)

  • ++a用在表达式中时,表达式的值是a自增后的值。

  • 自增操作会在表达式的其余部分计算之前执行。

  • int a = 5;
    int b = ++a; // a 首先变为 6,然后 b 被赋值为 6

关系运算符

若a=10,b=20,则:

运算符 描述 实例
== 检查两个操作数的值是否相等,如果相等则条件为真。 (a == b) 为假。
!= 检查两个操作数的值是否相等,如果不相等则条件为真。 (a != b) 为真。
> 检查左操作数的值是否大于右操作数的值,如果是则条件为真。 (a > b) 为假。
< 检查左操作数的值是否小于右操作数的值,如果是则条件为真。 (a < b) 为真。
>= 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。 (a >= b) 为假。
<= 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。 (a <= b) 为真。
#include <stdio.h>

int main()
{
   int a = 21;
   int b = 10;
   int c ;

   if( a == b )
   {
      printf("Line 1 - a 等于 b\n" );
   }
   else
   {
      printf("Line 1 - a 不等于 b\n" );
   }
   if ( a < b )
   {
      printf("Line 2 - a 小于 b\n" );
   }
   else
   {
      printf("Line 2 - a 不小于 b\n" );
   }
   if ( a > b )
   {
      printf("Line 3 - a 大于 b\n" );
   }
   else
   {
      printf("Line 3 - a 不大于 b\n" );
   }
   /* 改变 a 和 b 的值 */
   a = 5;
   b = 20;
   if ( a <= b )
   {
      printf("Line 4 - a 小于或等于 b\n" );
   }
   if ( b >= a )
   {
      printf("Line 5 - b 大于或等于 a\n" );
   }
}

逻辑运算符

在C语言中0为假(false),其他为为真(true)

若A=1,B=0

运算符 描述 实例
&& 称为逻辑与运算符。如果两个操作数都非零,则条件为真。 (A && B) 为假。
|| 称为逻辑或运算符。如果两个操作数中有任意一个非零,则条件为真。 (A || B) 为真。
! 称为逻辑非运算符。用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。 !(A && B) 为真。

对于逻辑运算符的前后我们往往会使用判断语句

#include <stdio.h>

int main()
{
   int a = 5;
   int b = 20;
   int c ;
//对于条件的判断来说,一个数非0就是真,0就是假
   if ( a==5 && b==20 )
   {
      printf("Line 1 - 条件为真\n" );
   }
   if ( a==5 || b==1 )
   {
      printf("Line 2 - 条件为真\n" );
   }
   /* 改变 a 和 b 的值 */
   a = 0;
   b = 10;
   if ( a!=0 && b==10 )
   {
      printf("Line 3 - 条件为真\n" );
   }
   else
   {
      printf("Line 3 - 条件为假\n" );
   }
   if ( !(a!=0 && b==10) )
   {
      printf("Line 4 - 条件为真\n" );
   }
}

赋值运算符

运算符 描述 实例
= 简单的赋值运算符,把右边操作数的值赋给左边操作数 C = A + B 将把 A + B 的值赋给 C
+= 加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数 C += A 相当于 C = C + A
-= 减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数 C -= A 相当于 C = C - A
*= 乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数 C = A 相当于 C = C A
/= 除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数 C /= A 相当于 C = C / A
%= 求模且赋值运算符,求两个操作数的模赋值给左边操作数 C %= A 相当于 C = C %A
#include <stdio.h>

int main()
{
   int a = 21;
   int c ;
   c =  a;
   printf("Line 1 - =  运算符实例,c 的值 = %d\n", c );
   c +=  a;
   printf("Line 2 - += 运算符实例,c 的值 = %d\n", c );
   c -=  a;
   printf("Line 3 - -= 运算符实例,c 的值 = %d\n", c );
   c *=  a;
   printf("Line 4 - *= 运算符实例,c 的值 = %d\n", c );
   c /=  a;
   printf("Line 5 - /= 运算符实例,c 的值 = %d\n", c );
   c  = 200;
   c %=  a;
   printf("Line 6 - %= 运算符实例,c 的值 = %d\n", c );
}

位运算

位运算符作用于位,并逐位执行操作。&、 | 和 ^ 的真值表如下所示:

p q p & q p \ q p ^ q
0 0 0 0 0
0 1 0 1 1
1 1 1 1 0
1 0 0 1 1

假设如果 A = 60,且 B = 13,现在以二进制格式表示,它们如下所示:

A = 0011 1100

B = 0000 1101

-----------------

A&B = 0000 1100

A|B = 0011 1101

A^B = 0011 0001

~A = 1100 0011

下表显示了 C 语言支持的位运算符。假设变量 A 的值为 60,变量 B 的值为 13,则:

运算符 描述 实例
& 对两个操作数的每一位执行逻辑与操作,如果两个相应的位都为 1,则结果为 1,否则为 0。按位与操作,按二进制位进行"与"运算。运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1; (A & B) 将得到 12,即为 0000 1100
| 对两个操作数的每一位执行逻辑或操作,如果两个相应的位都为 0,则结果为 0,否则为 1。按位或运算符,按二进制位进行"或"运算。运算规则:0|0=0; 0|1=1; 1|0=1; 1|1=1; (A | B) 将得到 61,即为 0011 1101
^ 对两个操作数的每一位执行逻辑异或操作,如果两个相应的位值相同,则结果为 0,否则为 1。异或运算符,按二进制位进行"异或"运算。运算规则:0^0=0; 0^1=1; 1^0=1; 1^1=0; (A ^ B) 将得到 49,即为 0011 0001
~ 对操作数的每一位执行逻辑取反操作,即将每一位的 0 变为 1,1 变为 0。取反运算符,按二进制位进行"取反"运算。运算规则:~1=-2; ~0=-1; (~A ) 将得到 -61,即为 1100 0011,一个有符号二进制数的补码形式。
<< 将操作数的所有位向左移动指定的位数。左移 n 位相当于乘以 2 的 n 次方。二进制左移运算符。将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。 A << 2 将得到 240,即为 1111 0000
>> 将操作数的所有位向右移动指定的位数。右移n位相当于除以 2 的 n 次方。二进制右移运算符。将一个数的各二进制位全部右移若干位,正数左补 0,负数左补 1,右边丢弃。 A >> 2 将得到 15,即为 0000 1111
#include <stdio.h>

int main()
{

   unsigned int a = 60;    /* 60 = 0011 1100 */  
   unsigned int b = 13;    /* 13 = 0000 1101 */
   int c = 0;           
   c = a & b;       /* 12 = 0000 1100 */ 
   printf("Line 1 - c 的值是 %d\n", c );
   c = a | b;       /* 61 = 0011 1101 */
   printf("Line 2 - c 的值是 %d\n", c );
   c = a ^ b;       /* 49 = 0011 0001 */
   printf("Line 3 - c 的值是 %d\n", c );
   c = ~a;          /*-61 = 1100 0011 */
   printf("Line 4 - c 的值是 %d\n", c );
   c = a << 2;     /* 240 = 1111 0000 */
   printf("Line 5 - c 的值是 %d\n", c );
   c = a >> 2;     /* 15 = 0000 1111 */
   printf("Line 6 - c 的值是 %d\n", c );
}

输出结果如下:

Line 1 - c 的值是 12
Line 2 - c 的值是 61
Line 3 - c 的值是 49
Line 4 - c 的值是 -61
Line 5 - c 的值是 240
Line 6 - c 的值是 15

问:如此不常规的位运算有什么作用呢?

答:位运算的用处多多,简单举一个例子吧!

洛谷的这道题目https://www.luogu.com.cn/problem/P1469,可以使用位运算快速解决。

#include "bits/stdc++.h"
using namespace std;
using ll = long long ;
const long long int N = 1e7+10;
ll sum;
ll b,c,d,e,f,g,o,p,q;
ll a[N];
ll diff[N],diff_01[N];
int main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    cin>>b;
    for(int i=1;i<=b;i++){
        cin>>c;
        d=d^c;//这要看懂这里即可,两个相等的数使用异或为0,而0和任意数的异或为那个非零数,这样可以快速将不同的数找出
    }
    cout<<d<<'\n';
}

预处理器

指令 描述
#define 定义宏
#include 包含一个源代码文件
#define pi 3.14

注意:define只是一种替换,它不会判断运算顺序,因此括号显得极其重要

#include <stdio.h>

#define square(x) ((x) * (x))

#define square_1(x) (x * x)

int main(void)
{
    printf("square 5+4 is %d\n", square(5+4));//这里是(5+4)*(5+4),结果为81
    printf("square_1 5+4 is %d\n", square_1(5+4));//这里是5+4*5+4,结果为29
    return 0;
}

sizeof()和其他杂项运算符

运算符 描述 实例
sizeof() 返回变量的大小。 sizeof(a) 将返回 4,其中 a 是整数。
& 返回变量的地址。 &a; 将给出变量的实际地址。
* 指向一个变量。 *a; 将指向一个变量。
? : 条件表达式 如果条件为真 ? 则值为 X : 否则值为 Y

sizeof()可以获取指定数据的大小,括号里门可以填写类型和变量。

三元表达式可以引出判断判断

#include <stdio.h>

int main()
{
    int a = 4;
    short b;
    double c;
    int* ptr;
    //sizeof()运算符
    printf("Line 1 - 变量 a 的大小 = %d\n", sizeof(a) );
    printf("Line 2 - 变量 b 的大小 = %d\n", sizeof(b) );
    printf("Line 3 - 变量 c 的大小 = %d\n", sizeof(c) );

    /* & 和 * 运算符实例 */
    ptr = &a;    /* 'ptr' 现在包含 'a' 的地址 */
    printf("a 的值是 %d\n", a);
    printf("ptr即a的地址为 %p\n",ptr);
    printf("*ptr 是 %d\n", *ptr);

    /* 三元运算符实例 */
    //以后写其他语言会经常使用。
    //Exp1 ? Exp2(如果Exp1为真) : Exp3(如果Exp1为假) ;
    a = 10;
    b = (a == 1) ? 20: 30;
    printf( "b 的值是 %d\n", b );

    b = (a == 10) ? 20: 30;
    printf( "b 的值是 %d\n", b );
}

注释

  1. 代码中有不需要的代码可以直接删除,也可以注释掉
  2. 代码中有些代码比较难懂,可以加一下注释文字

举个栗子:

#include <stdio.h>
int Add(int x, int y)
{
    return x + y;
}
/*C语言风格注释
int Sub(int x, int y)
{
    return x-y;
}
*/
int main()
{
    //C++注释风格
    //int a = 10;
    //调用Add函数,完成加法
    printf("%d\n", Add(1, 2));
    return 0;
}

注释有两种风格:

  • C语言风格的注释 /xxxxxx/ 缺陷:不能嵌套注释
  • C++风格的注释 //xxxxxxxx 可以注释一行也可以注释多行

判断语句

if语句

if语句中,首先判断表达式的值,然后根据该值的情况控制程序流程。表达式的值不等于0,即为真;否则为假。if语句有if,if--else和else if 三种形式

if(表达式) 语句
if(表达式)
{   
    语句块1;
}
else
{
    语句块2;
}
if(表达式1) 语句1
else if(表达式2) 语句2
····
else if(表达式n—1) 语句n-1
else 语句n

问:如果想要判断一个语句先满足一个条件,再满足一个条件,我们该怎么办呢?

答:我们可以使用if的嵌套结构。

if(表达式1)
{
    if(表达式2)
    {
        语句块1;
    }
    else
    {
        语句块2;        
    }
{
else
{
    if(表达式3)
    {
        语句块3;
    }
    else
    {
        语句块4;        
    }
}

条件运算符

条件运算符可对一个表达式的值的真假情况进行检验,然后根据检验结果返回另外两个表达式中的一个。

表达式1?表达式2:表达式3;
max=(a>b)?a:b;

在运算中,首先对第一个表达式的值进行检验。如果值为,则返回第二个表达式的结果值;如果为,则返回第三个表达式的结果值

例:a>b为真,则max=a;为假,则max=b

这种判断符一般被称为三元运算符,或者说就是三目运算符

switch语句

if只有两个分枝可供选择,而实际情况中常需要用到多分枝的选择。当然,使用嵌套的if语句也可以实现多分枝的选择,但是如果分枝较多,就会使得嵌套的if语句层数较多,程序冗余,并且可读性不好。C语言中可以使用switch语句直接处理多分枝选择的情况,提高程序代码可读性。

switch(表达式)
{
    case:1
        语句块;break;
    case:2
        语句块;break;
    。。。
    case:n
        语句块;break;
    default:
        默认情况语句块;break;
}

表达式的结果必须为整数

default关键字的作用是如果没有符合条件的情况,那么执行default后的默认情况语句,default可以省略

switch 语句必须遵循下面的规则:

  • switch 语句中的 表达式 是一个常量表达式,必须是一个整型或枚举类型。
  • 在一个 switch 中可以有任意数量的 case 语句。每个 case 后跟一个要比较的值和一个冒号。
  • case 的 条件 必须与 switch 中的变量具有相同的数据类型,且必须是一个常量或字面量。
  • 当被测试的变量等于 case 中的常量时,case 后跟的语句将被执行,直到遇到 break 语句为止。
  • 当遇到 break 语句时,switch 终止,控制流将跳转到 switch 语句后的下一行。
  • 不是每一个 case 都需要包含 break。如果 case 语句不包含 break,控制流将会 继续 后续的 case,直到遇到 break 为止。
  • 一个 switch 语句可以有一个可选的 default case,出现在 switch 的结尾。default case 可用于在上面所有 case 都不为真时执行一个任务。default case 中的 break 语句不是必需的。

那么结合下面的注释思考一下下面的代码。

#include <stdio.h>

int main()
{
    int a;
    printf("input integer number: ");
    scanf("%d",&a);
    switch(a)//这个地方的a必须是应该整型或者枚举类
    {
        case 1:printf("Monday\n");//case 后边的数据的类型和上面的a保持一致
            break;//break,是要加的,如果我们不加这个,输入1会发生什么呢?
        case 2:printf("Tuesday\n");
            break;
        case 3:printf("Wednesday\n");
            break;
        case 4:printf("Thursday\n");
            break;
        case 5:printf("Friday\n");
            break;
        case 6:printf("Saturday\n");
            break;
        case 7:printf("Sunday\n");
            break;
        default:printf("error\n");
    }
}

循环语句

C语言有三大循环语句,他们之间可以进行任意转换。 我们将首先对其语法进行讲解,然后通过一个实例用三种循环来实现。相信通过本文的学习,大家都能够对C语言循环语句有着熟练的掌握。

do…while() 循环

图示流程

https://img-blog.csdnimg.cn/20200810095628553.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0FOX2RyZXc=,size_16,color_FFFFFF,t_70

从上面图示 do…while() 语句流程中可以看出,do…while() 语句会先进入循环体执行里面的内容,然后再进行条件判断,当条件为真,就继续执行循环体的内容,当条件为假就退出do…while() 语句。也就是说 do…while() 语句 最少会执行一遍循环体里面的内容。

代码上来说:

do {
    语句块
} while (表达式);

do…while() 语句的代码流程也很简单,程序执行到 do…while() 语句的时候,会先执行语句块(也叫循环体)中的内容,执行完一次后,就会判断表达式的内容是真还是假,如果是真,那么就继续执行语句块的内容,如果是假,那么就不再执行语句块的内容,而是退出该循环。在写 do…while() 语句的时候 while 后面那个分号千万不能掉了,这点新手尤其要注意。

我们通过一个简单的题目练习一下:

将1到100进行求和。

https://img-blog.csdnimg.cn/20200810100404672.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0FOX2RyZXc=,size_16,color_FFFFFF,t_70

#include <stdio.h>

int main()
{
    int i = 1, sum = 0;

    do {
        sum += i;//在判断i<=100前我们就执行了这内部的语句
        i++;
    } while (i <= 100);

    printf("sum = %d\n", sum);

    return 0;
}

while()循环

图例流程

https://img-blog.csdnimg.cn/20200810010402647.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0FOX2RyZXc=,size_16,color_FFFFFF,t_70

while() 循环语句会先判断条件,当条件为真的时候才会执行循环体,当条件为假的时候直接就退出了循环体。也就是说,while() 语句循环体里面的内容可能一次都不会被执行,这就是 while() 语句和 do…while() 语句最大的区别。

代码流程

while (表达式) {
    语句块
}

while() 循环语句的代码流程也很简单,就是先判断表达式的内容,当表达式为真的时候,就执行语句块的内容,语句块中的内容执行完了后又会判断表达式的值,直到表达式的值为假才会跳出语句块中。

我们还是通过那个简单的题目练习一下:

将1到100进行求和。

https://img-blog.csdnimg.cn/20200810011137730.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0FOX2RyZXc=,size_16,color_FFFFFF,t_70

#include <stdio.h>

int main()
{
    int i = 0, sum = 0;

    while (i <= 100) {
        sum += i;
        i++;
    }

    printf("sum = %d\n", sum);

    return 0;
}

for()循环

流程演示

https://img-blog.csdnimg.cn/2020081001203581.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0FOX2RyZXc=,size_16,color_FFFFFF,t_70

for() 循环的图示代码流程和 while() 循环的图示代码流程不能说毫不相干,只能说一模一样。但是其代码表现流程有点区别,下面来重点讲解下for() 循环的代码流程。

代码流程

for (表达式1; 表达式2; 表达式3) 
{
    语句块
}

for() 循环的代码流程看着表达式挺多的,好像挺复杂,但其实不然,让我来为大家进行细致讲解。
for() 循环首先执行表达式1,再执行表达式2,当表达式2的值为真的时候就会执行语句块的内容,语句块内容执行完后就会执行表达式3,表达式3执行完,又会跳转执行表达式2,当表达式2为真,又执行语句块,相当于循环一直在 表达式2 -> 语句块 -> 表达式3 之间循环。当表达式2的值为假的时候就会跳出循环。
for() 循环有几个地方值得大家注意:
(1)表达式1只会在刚进 for 循环的时候执行一次。
(2)在c99及之后的标准中,表达式1处可以定义变量,变量周期在整个for循环中。但是c98不允许这样做,否则编译器会报错。
(3)表达式1、表达式2、表达式3 都可以不写省略。但是当表达式2省略不写的时候意味着,编译器在处理这里的时候这里不为假,从而会执行语句块。

没错,我们还是通过那个简单的题目练习一下:

将1到100进行求和。

https://img-blog.csdnimg.cn/20200810012052234.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0FOX2RyZXc=,size_16,color_FFFFFF,t_70

#include <stdio.h>

int main()
{
    int i = 1, sum = 0;

    for (i = 0; i <= 100; i++) {
        sum += i;
    }    
    printf("sum = %d\n", sum);

    return 0;
}

continue语句

在上面的三个循环语句中都可以使用continue语句。那么他的作用是什么呢?

continue语句会跳过在它之后的循环体,跳转到下一次循环判断

emm,可以不好理解,那我们举个栗子吧

还是借用前面的题目,但是稍微修改一下,这次我们求从1到100中偶数项之和。

https://img-blog.csdnimg.cn/2020081012551464.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0FOX2RyZXc=,size_16,color_FFFFFF,t_70

#include <stdio.h>

int main() {
    int sum = 0;
    for (int i = 1; i <= 100; i++) {
        if (i % 2 != 0) {
            continue; // 如果是奇数,则跳过后面的代码,直接进行下一次循环
        }
        sum += i; // 只有当i是偶数时,才会执行到这里
    }
    printf("The sum of odd numbers from 1 to 100 is: %d\n", sum);
    return 0;
}

break语句

和continue语句一样可以用于三种循环结构。

break语句可以退出当前循环(在多层嵌套循环中仅能退出一层循环)

还是举个栗子

求一个数是不是素数

https://img-blog.csdnimg.cn/e57cbc58a468492895eed3e7e8d06b8a.png

#include <stdio.h>

int main() {
    int n;
    printf("Enter a number: ");
    scanf("%d", &n);

    int i;
    for (i = 2; i < n; i++) {
        if (n % i == 0) {
            break;
        }
    }

    if (i == n) {
        printf("%d is a prime number.\n", n);
    } else {
        printf("%d is not a prime number.\n", n);
    }

    return 0;
}

函数

什么是函数

提到函数,我们最先想到的肯定是数学中的函数,那么C语言中的函数究竟是什么呢?接下来带大家看一下吧!

维基百科中对函数的定义:子程序在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method, subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组 成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。 一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。

看完是不是直接阿巴阿巴了,没事我们一步步进行了解。

函数的分类

库函数

问:为什么需要库函数?

答:

  • 我们知道在我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格 式打印到屏幕上(printf)。
  • 在编程的过程中我们会频繁的做一些字符串的拷贝工作(strcpy)。
  • 在编程是我们也计算,总是会计算n的k次方这样的运算(pow)。

所以,像上面我们描述的基础功能,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到, 为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。

问:那么哪些是库函数呢?

答:举个简单的例子,库函数就是C语言本身给我们已经定义好的函数,作为程序员我们可以直接使用,就像printf()和scanf()。

注意:使用库函数必须包含头文件,例如我们使用printf()与scanf()时要引用stdio.h头文件,即我们通常写的#include

常见的库函数有:

  • IO函数(输入输出函数)
  • 字符串操作函数
  • 字符操作函数
  • 内存操作函数
  • 时间/日期函数
  • 数学函数
  • 其他库函数

这个地方不会展开去讲,后期用到的时候具体讲解。

自定义函数

问:什么是自定义函数,又为什么需要自定义函数呢

答: 自定义函数就是程序员自己定义用于首先特定功能的函数!自定义函数和库函数一样,有函数名,返回值类型和函数参数。

但是不一样的是这些都是我们自己来设计。这给程序员一个很大的发挥空间。

结构大体如下:

ret_type fun_name(para1, *)
{
    statement;//语句项
}
ret_type 返回类型
fun_name 函数名
para1    函数参数

举个栗子

设计一个返回最大值的函数

#include <stdio.h>
//get_max函数的设计
int get_max(int x, int y)
{
    return (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;
}

函数的参数

实际参数(实参)

问:实参是什么呢?

答:真实传给函数的参数,叫实参。 实参可以是:常量、变量、表达式、函数等。

注意:为什么可以是函数呢?因为有的函数是由返回值的,所以自然也就能充当实参。

无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。

形式参数(形参)

问:形参和实参有什么区别呢?

答:形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单 元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。

为什么这样说呢?接下来给大家举个例子吧!

例如我们要交换两个变量的值:

#include <stdio.h>
void swap(int x, int y)//这个地方传入的是形式变量
{
    int temp = x;
    x = y;
    y = temp;
}
int main()
{
    int num1 = 10;
    int num2 = 20;
    swap(num1, num2);
    printf("num1 = %d\nnum2 = %d", num1,num2);//结果为num1 = 10 num2 = 20
    return 0;
}

很明显,并没有达成交换的目的,这就证明了:形式参数当函数调用完成之后就自动销毁了,即我们把num1和num2传给x和y之后,虽然我们在函数例将x和y交换了,但是由于x和y在swap函数调用完成后就销毁了,即并没有真正实现num1和num2的交换。

所以我们可以简单的认为:形参实例化之后其实相当于实参的一份临时拷贝。

函数的调用

传值调用

函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。我们上述的交换的例子就是传值调用,即并不能真正达成交换两个变量的值的目的!

传址调用

传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。

这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操 作函数外部的变量。

同样我们还是采用之前交换值的例子来说明

#include <stdio.h>
void swap(int *x, int *y)
{
    int temp = *x;
    *x = *y;
    *y = temp;
}
int main()
{
    int num1 = 10;
    int num2 = 20;
    swap(&num1, &num2);
    printf("num1 = %d\nnum2 = %d", num1,num2);
    return 0;
}

那么为什么会是这样呢?等后期我们学到指针的那一节的时候将会具体讲解!

函数的嵌套调用和链式访问

嵌套调用

那么函数是如何进行嵌套调用的呢?下面给大家举个例子吧!

#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;
}

在上述代码中,在main()函数中调用了three()函数,而在three()函数中又调用了new_line()函数,这就是嵌套调用,即在一个函数中调用另一个函数。

特别注意:C语言中,函数可以嵌套调用,但是不能嵌套定义!

链式访问

那么什么是链式访问呢?通俗来讲,就是把一个函数的返回值作为另外一个函数的参数。

貌似与嵌套调用挺像的,但两者是存在区别的:嵌套调用是在函数中调用函数,而链式访问则是将一个函数的返回值作为另一个函数的参数

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

打印结果是什么呢?最后的打印结果是4321,为什么呢?

此处给大家普及一个知识点,printf()函数的返回值是正确输出在屏幕上的字符数,例如,输出在屏幕上的数字是43,此时函数的返回值就是2,如果打印在屏幕上的数字是2的话,返回值就是1,那么此处输出的结果也就不难理解了。

另外一个给大家补充的点是:在上面这个函数中,究竟是如何进行执行的呢?首先执行的是最外层的printf()函数,而最外层函数的返回值依赖于次外层函数的返回值,而次外层函数的返回值又依赖于内层函数的返回值,就是层层向内调用,然后层层返回。printf("%d",43)的返回值是2,而printf("%d",2)的返回值是1,所以最终输出在屏幕上的就是4321,因为最内层函数先执行进行输出的,所以也可以理解成是由内向外进行执行的,但调用的顺序却是由外向内的。

函数的声明与定义

函数的声明

  • 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数 声明决定不了。
  • 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
  • 函数的声明一般要放在头文件中的。

函数的定义

函数的定义是指函数的具体实现,交代函数的功能实现。

我们在设计程序的时候,一般在头文件的部分进行函数的声明,在程序的末尾进行函数的定义。

#include <stdio.h>
//函数的声明
int Add(int x, int y);

int main()
{
    printf("%d", Add(3, 4));
    return 0;
}

//函数Add的实现
int Add(int x, int y)
{
 return x+y;
}

当我们设计多文件程序的时候,一般在以.h为结尾的头文件内放置函数的声明,在.c源文件内放置函数的定义。

test.h的内容

放置函数的声明

#ifndef __TEST_H__
#define __TEST_H__
//函数的声明
int Add(int x, int y);
#endif //__TEST_H__

test.c的内容

放置函数的实现

#include "test.h"
//函数Add的实现
int Add(int x, int y)
{
 return x+y;
}

函数的递归

递归的定义

问:什么是递归?

答:程序调用自身的编程技巧称为递归( recursion)。

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

递归的主要思考方式在于:把大事化小

递归的两个必要条件

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

递归吗?没太多好讲的上题目演示吧

接受一个整型值(无符号),按照顺序打印它的每一位。

例如: 输入:1234,输出 1 2 3 4

解:

#include <stdio.h>
void print(int n)
{
    if (n > 9)
    {
        print(n / 10);//这句就是递归公式,每次除以10
    }
    printf("%d ", n % 10);//这句就是递归出口,为一位数的时候进行输出
}
int main()
{
    int num;
    scanf("%d",&num);
    print(num);
    return 0;
}

递归与迭代

求第n个斐波那契数。(不考虑溢出)

int fib(int n)
{
    if (n <= 2)
        return 1;
    else
        return fib(n - 1) + fib(n - 2);
}

但是我们发现有问题:

在使用 fib 这个函数的时候如果我们要计算第50个斐波那契数字的时候特别耗费时间。
使用 factorial 函数求10000的阶乘(不考虑结果的正确性),程序会崩溃。
为什么呢?

我们发现 fib 函数在调用的过程中很多计算其实在一直重复。

如何验证其重复了很多次呢?

我们这样写

int count = 0;//全局变量
int fib(int n)
{
    if (n == 3)
        count++;
    if (n <= 2)
        return 1;
    else
        return fib(n - 1) + fib(n - 2);
}

我们随意传一个数就会发现count是一个相当大的值。

那我们如何改进呢?

  • 在调试 factorial 函数的时候,如果你的参数比较大,那就会报错: stack overflow(栈溢出) 这样的信息。 系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一 直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。

那如何解决上述的问题:

  1. 将递归改写成非递归即迭代。
  2. 使用static对象替代 nonstatic 局部对象。在递归函数设计中,可以使用 static 对象替代 nonstatic 局部对象(即栈对象),这不 仅可以减少每次递归调用和返回时产生和释放 nonstatic 对象的开销,而且 static 对象还可以保 存递归调用的中间状态,并且可为 各个调用层所访问。
//求n的阶乘
int factorial(int n)
{
    int result = 1;
    while (n > 1)
    {
        result *= n;
        n -= 1;
    }
    return result;
}
//求第n个斐波那契数
int fib(int n)
{
    int result;
    int pre_result;
    int next_older_result;
    result = pre_result = 1;
    while (n > 2)
    {
        n -= 1;
        next_older_result = pre_result;
        pre_result = result;
        result = pre_result + next_older_result;
    }
    return result;
}

提示:

  1. 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
  2. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
  3. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。

数组

一维数组

一维数组的创建

数组是一组相同类型元素的集合。

数组的创建方式:

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

例子:

//代码1
int arr1[10];
//代码2
int count = 10;
int arr2[count];//数组时候可以正常创建?
注意:此时是不可以创建的,因为count是变量,而[]内只能是常量
//代码3
char arr3[10];
float arr4[1];
double arr5[20];

注:数组创建,在C99标准之前, [] 中要给一个常量才可以,不能使用变量。在C99标准支持了变长数组的概念,即在[]中可以用变量。

一位数组的初始化

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

下面是例子:

int arr1[10] = { 1,2,3 };
int arr2[] = { 1,2,3,4 };
int arr3[5] = { 1,2,3,4,5 };
char arr4[3] = { 'a',98, 'c' };
char arr5[] = { 'a','b','c' };
char arr6[] = "abcdef";

数组在创建的时候如果想不指定数组的确定的大小就得初始化。数组的元素个数根据初始化的内容来确定。

一维数组的使用

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

#include<stdio.h>
int main()
{
    int arr[10] = { 0 };//数组的不完全初始化
       //计算数组的元素个数
    int sz = sizeof(arr) / sizeof(arr[0]);//这是求数组的长度,将数组的总长度除以单个元素的长度,我们就能够得到数组的大小
    //对数组内容赋值,数组是使用下标来访问的,下标从0开始。所以:
    int i = 0;//做下标
    for (i = 0; i < 10; i++)
    {
        arr[i] = i;
    }
    //输出数组的内容
    for (i = 0; i < 10; ++i)
    {
        printf("%d ", arr[i]);
    }
    return 0;
}

小结一下:

  1. 数组是使用下标来访问的,下标是从0开始。
  2. 数组的大小可以通过计算得到。
一维数组的存储

下面这段代码可以知道一维数组的存储

#include <stdio.h>
int main()
{
    int arr[10] = { 0 };
    int i = 0;
    int sz = sizeof(arr) / sizeof(arr[0]);

    for (i = 0; i < sz; ++i)
    {
        printf("&arr[%d] = %p\n", i, &arr[i]);
    }
    return 0;
}

https://cdn.jsdelivr.net/gh/nicedream18/picx-images-hosting@master/20240116/image.7m8lstnltsw.webp

仔细观察输出的结果,我们知道,随着数组下标的增长,元素的地址,也在有规律的递增。 由此可以得出结论:数组在内存中是连续存放的。

这个地方需要大家记住这个结论,因为后面我们还会讨论二维数组在内存空间中的存放!

以下为画图演示

https://cdn.jsdelivr.net/gh/nicedream18/picx-images-hosting@master/20240116/image.2ri4xw87xo60.webp

二维数组

二维数组的创建
//数组创建
int arr[3][4];
char arr[3][5];
double arr[2][4];
二维数组的初始化
//数组初始化
int arr[3][4] = {1,2,3,4};
int arr[3][4] = {{1,2},{4,5}};
int arr[][4] = {{2,3},{4,5}};//二维数组如果有初始化,行可以省略,列不能省略
二维数组的使用
#include <stdio.h>
int main()
{
    int arr[3][4] = { 0 };
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        int j = 0;
        for (j = 0; j < 4; j++)
        {
            arr[i][j] = i * 4 + j;
        }
    }
    for (i = 0; i < 3; i++)
    {
        int j = 0;
        for (j = 0; j < 4; j++)
        {
            printf("%d ", arr[i][j]);
        }
    }
    return 0;
}

以下为运行结果

https://cdn.jsdelivr.net/gh/nicedream18/picx-images-hosting@master/20240116/image.4ktcxjtlm5y0.webp

二维数组的存储

模仿上面对于一维数组的地址输出,对二维数组进行输出

https://cdn.jsdelivr.net/gh/nicedream18/picx-images-hosting@master/20240116/image.4t6d3by3p440.webp

从结果不难看出二维数组也是连续存储的。

存储图示大致如下

https://cdn.jsdelivr.net/gh/nicedream18/picx-images-hosting@master/20240116/image.79jcaz3lppw0.webp

数组越界

数组的下标是有范围限制的。

数组的下规定是从0开始的,如果数组有n个元素,最后一个元素的下标就是n-1。 所以数组的下标如果小于0,或者大于n-1,就是数组越界访问了,超出了数组合法空间的访问。

C语言本身是不做数组下标的越界检查,编译器也不一定报错,但是编译器不报错,并不意味着程序就是正确的, 所以程序员写代码时,最好自己做越界的检查。

#include <stdio.h>
int main()
{
    int a[10] ={1,2,3,4,5,6,7,8,9,10} ;
    for(int i =0;i<=10;i++){
        printf("%d ",a[i]);//这里当i=10时候就已经发生了越界,所以a[10]我们无法控制其输出
    }
    return 0;
}

下图为输出结果https://cdn.jsdelivr.net/gh/nicedream18/picx-images-hosting@master/20240116/image.6vfuwdytbs80.webp

数组作为参数

往往我们在写代码的时候,会将数组作为参数传个函数,比如:我要实现一个冒泡排序(这里要讲算法 思想)函数将一个整形数组排序。

那我们将会这样使用该函数:

#include <stdio.h>
void bubble_sort(int arr[],int sz)//冒泡排序
{
    int i = 0;
    for (i = 0; i < sz - 1; i++)
    {
        int j = 0;
        for (j = 0; j < sz - i - 1; j++)
        {
            if (arr[j] > arr[j + 1])
            {
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
            }
        }
    }
}
int main()
{
    int arr[] = { 3,1,7,5,8,9,0,2,4,6 };
    int sz = sizeof(arr) / sizeof(arr[0]);//获取对应长度
    bubble_sort(arr,sz);
    for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
    {
        printf("%d ", arr[i]);
    }
    return 0;
}

那么接下来我探究一下数组名究竟是什么

#include <stdio.h>
int main()
{
    int arr[10] = { 1,2,3,4,5 };
    printf("%p\n", arr);
    printf("%p\n", &arr[0]);
    printf("%d\n", *arr);
    //输出结果
    return 0;
}

以下为输出结果

https://cdn.jsdelivr.net/gh/nicedream18/picx-images-hosting@master/20240116/image.6huhpx5xlrw0.webp

所以结论:数组名是数组首元素的地址。(有下面两个例外)

  • sizeof(数组名),计算整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数组。

  • &数组名,取出的是数组的地址。&数名,数组名表示整个数组。

    除此1,2两种情况之外,所有的数组名都表示数组首元素的地址。