C语言知识的重新整合(二)

简介: C语言知识的重新整合(二)

作用域

作用域是程序设计概念,通常来说,一个变量在哪里可以使用,哪里就是它的作用域

  • 局部变量的作用域是变量所在的局部范围即{}内
  • 全局变量的作用域是整个工程

局部变量的作用域是变量所在的局部范围

全局变量的作用域是整个工程

生命周期

生命周期指的是变量的创建(内存申请)到变量的销毁(收回内存)之间的一个时间段

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

C语言存储类

静态存储类static

static修饰局部变量

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

结论:

static修饰局部变量改变了变量的生命周期,生命周期改变的本质是改变了变量的存储类型,本来一个局部变量是存储在内存的栈区的,但是被static修饰后存储到了静态区,存储在静态区的局部变量等价于全局变量,即程序结束->变量销毁->内存回收

静态变量分为:静态局部变量静态全局变量

  • 静态局部变量-被static修饰的局部变量
  • 静态全局变量-被static修饰的全局变量

static修饰全局变量

test.c文件

#include <stdio.h>
extern int g;
int main()
{
  printf("%d", g);
  return 0;
}

all_test.c文件(全局变量未被static修饰)

all_test.c文件(全局变量未被static修饰)

结论:⼀个全局变量被static修饰,使得这个全局变量只能在本源⽂件内使⽤,不能在其他源⽂件内使⽤。本质原因是全局变量默认是具有外部链接属性(external linkage),在外部的⽂件中想使⽤,只要适当的声明就可以使⽤;但是全局变量被static修饰之后,外部链接属性就变成了内部链接属性,只能在⾃⼰所在的源⽂件内部使⽤了,其他源⽂件,即使声明了,也⽆法正常使⽤

static修饰函数:

结论:与static修饰全局变量一样,当⼀个函数被static修饰,使得这个函数只能在本源⽂件内使⽤,不能在其他源⽂件内使⽤。本质原因是函数默认是具有外部链接属性的,在外部的⽂件中想使⽤,只要适当的声明就可以使⽤;但是函数被static修饰之后,外部链接属性就变成了内部链接属性,只能在⾃⼰所在的源⽂件内部使⽤了,其他源⽂件,即使声明了,也是⽆法正常使⽤的

外部存储类extern

特点:只能修饰全局变量和外部函数(其它源文件中的函数)

       需要补充的是全局变量和外部函数都默认拥有外部链接属性(external linkage),所以即使不用extern修饰全局变量它也能在同一项目下的多个源文件之间使用,只是说使用extern修饰可以增强代码的可读性和清晰性

自动存储类auto

在现代的 C 和 C++ 中,它已经不再是必需的,因为局部变量的存储类别默认就是 auto

寄存器存储类register

register 是一个存储类别说明符,用于向编译器建议将变量存储在寄存器中,以便提高访问速度。在 C 和 C++ 中,register 关键字可以用于修饰局部变量,但它的使用已经不常见,并且在现代的编译器中往往不会产生显著的性能改进

C语言变量和常量

在任务工单一中1讲述了C语言的数据类型,那么类型的作用是什么?

答:创建变量

变量就是经常变化的量,不变的我们另称之为常量

变量

变量的创建与初始化

创建方式:数据类型   变量名

//初始化

int age = 18  //整型变量

char ch = ‘w'//字符变量

double weight = 48.0  //浮点型变量

变量的分类:

变量分为全局变量局部变量

大括号{}之外定义的变量就是全局变量,它的使用范围可以覆盖整个工程

大括号{}之内定义的变量就是全局变量,它的使用范围只能在局部范围内

#include<stdio.h>
int n = 1000;  //全局变量
int main()
{
  int n = 10; //局部变量
  printf("%d",n);
  return 0;
}

当全局变量和局部变量重名时,局部变量优先使用

常量

常量分为:字面常量、const修饰的常变量、#define宏定义的标识符常量、枚举常量

字面常量

字面常量主要包括:整型字面常量、浮点型字面常量、字符字面常量、字符串字面常量

整型字面常量

整型字面常量可以是十进制、八进制和十六进制的常量

前缀为0x 或 0X 表示十六进制,为0 表示八进制,不带前缀则默认表示十进制;

后缀可以由U(unsigned)L (long)组成,大小写随意,顺序随意

85         /* 十进制 */

0213       /* 八进制 */

0x4b       /* 十六进制 */

30         /* 整数 */

30u        /* 无符号整数 */

30l        /* 长整数 */

30ul       /* 无符号长整数 */

!八进制数字只有0,1,2,3,4,5,6,7!

浮点字面常量

浮点常量包括:整数部分、小数点、小数部分、指数部分】

以使用小数形式或者指数形式来表示浮点常量。

当使用小数形式表示时,必须包含整数部分、小数部分,或同时包含两者。当使用指数形式表示时, 必须包含小数点、指数,或同时包含两者。带符号的指数是用 e 或 E 引入的。

3.14159       /* 合法的 */

314159E-5L    /* 合法的 */


其中,浮点数常量可以带有一个后缀表示数据类型,例如:float myFloat = 3.14f;

字符字面常量

字符常量包括:普通字符和转义字符

普通字符——用单引号括起来的一个字符

'a'  'b'  '!'

字符常量在储存在计算机的储存单元中时,一般采用ASCII代码储存的

int main()
{
  char a = 'a';
  printf("%d",a);
  return 0;
}

此链接可以跳转至ASCII码表:ASCII码对照表 (oschina.net)

转义字符——特殊字符常量

转义字符是C语言中表示字符的一种特殊形式,其含义是将反斜杠后面的字符转换成另外的意义。

int main()
{
  printf("Hello\t\nWorld");
  return 0;
}

字符串字面常量

字符串字面常量就是用双引号括起来的字符

int main()
{
  char a[] = "hello world";
  char b[] = "hello \
    world";
  char c[] = "hello ""w""orld";
  printf("%s\n", a);
  printf("%s\n", b);
  printf("%s\n", c);
  return 0;
}

注意事项:

1、字符串常量在内存中读取时遇到 \0 结尾,且系统自动在字符串末尾添加不会显示的\0

2、可以自主添加\0

int main()
{
  char a[] = "hello\0world";
  printf("%s\n", a);
  return 0;
}

const修饰的常变量

使用方式:const 数据类型 变量名 = 变量值

通俗来讲就是,被const关键字修饰后的变量就不能被修改的常量,重新赋值就报错:

int main()
{
  const int a = 5;
  a = 10;
  printf("%d", a);
  return 0;
}

#define宏定义的标识符常量

使用方式:#define 常量名 常量值

#define的多种用法及注意事项:

1、定义常量

#define MAX 1000

#define STR   “hehe”

2、简化名称

#define reg register // register这个关键字,创建⼀个简短的名字

3、效果替换

#define do_forever for(;;) //⽤更形象的符号来替换⼀种实现

for(;;)中判断条件为空则语句陷入死循环

4、简略代码

#define CASE break;case //在写case语句的时候⾃动把 break写上。

5、如果定义的stuff过⻓,可以分⾏写,除了最后⼀⾏外,每⾏的后⾯加⼀个反斜杠(续⾏符)

#define DEBUG_PRINT  printf("file:%s\tline:%d\t \

date:%s\ttime:%s\n" ,\

__FILE__,__LINE__ , \

__DATE__,__TIME__ )

在define定义标识符的时最后不加分号 

#define定义宏

#define机制包括了⼀个规定,允许把参数替换到⽂本中,这种实现通常称为宏(macro)或定义宏

(define macro)

声明方式: #define name( parament-list ) stuff

其中的 parament-list 是⼀个由逗号隔开的符号表,它们可能出现在stuff中。

参数列表的左括号必须与name紧邻,如果两者之间有空⽩,参数列表就会被视为stuff的⼀部分

#define SQUARE(X) X*X
int main()
{
  int a = 5;
  printf("%d\n", SQUARE(a));
  return 0;
}

但是,这样的书写方式还可能会造成一些问题,当我们将括号内的a改为a+2时:

#define SQUARE(X) X*X
int main()
{
  int a = 5;
  printf("%d\n", SQUARE(a+2));
  return 0;
}

结果为17而非49,这是因为a+2在进行替代时的结果是:a+2*a+2 而非(a+2)*(a+2)

所以我们需要在宏定义的时候加上括号:#define SQUARE(X) (X)*(X)

结论:⽤于对数值表达式进⾏求值的宏定义都视情况加上括号,避免在使⽤宏时由于参数中的操作符或邻近操作符之间不可预料的相互作⽤。

带有副作用的宏参数

什么时带有副作用的宏参数?大概意思就是在得到想要的结果的同时产生了其它的效果:

int main()
{
  int a = 10;
  int b = a + 1;//结果为b=11,a=10
  int a = 10;
  int b = a++;//结果为b=11,a=1
  return 0;
}

       在这里我们的确使得b变为了11但是a+1和++a两种方式的不同导致了最后a的结果不同,这就叫副作用。

例子:求两个整数的较大值

//更改前
#define MAX(a,b) ((a)>(b)?(a):(b))
int main()
{
  int m = MAX(5,9);
  printf("%d", m);
  return 0;
}
//更改后
#define MAX(a,b) ((a)>(b)?(a):(b))
int main()
{
  int a = 15;
  int b = 9;
  int m = MAX(a++,b++);
  //15 > 9 ? 16
  printf("%d\n", m);//16
  printf("a=%d b=%d\n",a,b);//17 10
  return 0;
}

这是因为替换后的m为:((a++)>(b++)?(a++):(b++)) ,在(a++)>(b++)时++并不起作用直接带入15和9,15大于9,这相当于使用过了所以之后执行++,此时a为16,则m的结果为16,但是由于输出的是a++所以此时a要再次进行++,之前的b也要进行++操作(在大于判断结束即b使用完后就++了)所以输出结果为a=17,b=10;

结论:当宏参数在宏的定义中出现超过⼀次的时候,如果参数带有副作⽤,那么你在使⽤这个宏的时候就可能出现危险,导致不可预测的后果。副作⽤就是表达式求值的时候出现的永久性效果

~未完待续~

       对于这两种定义常量的办法,建议使用 const 关键字来定义常量,因为它具有类型检查和作用域的优势,而 #define 仅进行简单的文本替换,可能会导致一些意外的问题:

  • 替换机制:#define 是进行简单的文本替换,而 const声明一个具有类型的常量#define 定义的常量在编译时会被直接替换为其对应的值,而 const 定义的常量在程序运行时会分配内存,并且具有类型信息。
  • 类型检查:#define不进行类型检查,因为它只是进行简单的文本替换。而 const 定义的常量具有类型信息,编译器可以对其进行类型检查。这可以帮助捕获一些潜在的类型错误。
  • 作用域:#define 定义的常量没有作用域限制,它在定义之后的整个代码中都有效。而 const 定义的常量具有块级作用域,只在其定义所在的作用域内有效。
  • 调试和符号表:使用 #define 定义的常量在符号表中不会有相应的条目,因为它只是进行文本替换。而使用 const 定义的常量会在符号表中有相应的条目,有助于调试和可读性。

符号表:在计算机科学中,符号表是一种用于语言翻译器(例如编译器解释器)中的数据结构。在符号表中,程序源代码中的每个标识符都和它的声明或使用信息绑定在一起,比如其数据类型、作用域以及内存地址

枚举常量

枚举通俗来讲就是一一列举,它可以用来列举我们日常生活中常见的各种东西,比如性别里的男和女,颜色里的赤橙黄绿青蓝紫,它的声明也很简单:

enum 类型名

{

枚举常量(建议全部大写)

};

enum Sex//性别
{
  MALE,
  FEMALE,
  SECRET
};
enum Color//颜⾊
{
  RED,
  GREEN,
  BLUE
};

其中,enum Color和enum Sex叫做枚举类型,MALE、FEMALE等叫做枚举常量,它们是枚举类型的可能取值。我们打印一下它们看看:

enum Sex//性别
{
  MALE,
  FEMALE,
  SECRET,
  NO=5
};
enum Color//颜⾊
{
  RED,
  GREEN,
  BLUE
};
int main()
{
  enum Sex sex = MALE; 
  printf("%d %d %d %d", MALE, FEMALE, SECRET,NO);
  return 0;
}

我们会发现打印结果为0 1 2,这是以为这些枚举常量都是有值,默认从0开始,依次递增1,当然在声明枚举类型的时候也可以赋初值

枚举类型的优点:

1、增加代码的可读性和可维护性

2、和#define定义的标识符⽐较枚举有类型检查,更加严谨。

3、便于调试,预处理阶段会删除 #define 定义的符号

4、使⽤⽅便,⼀次可以定义多个常量

5、枚举常量是遵循作⽤域规则的,枚举声明在函数内,只能在函数内使⽤

C语言运算符

算术运算符

包含内容:+、-、*、/、%、++、--

对于+、-、*不做过多赘述,它们都是有两个操作数的双目运算符即 操作数  算术运算符 操作数

/算术运算符

如果两边操作数为整数则结果为整数,即使将变量定义为浮点类型结果仍然不会是1.5,这是因为 C 语⾔⾥⾯的整数除法默认为整除,只会返回整数部分,丢弃⼩数部分。 如果希望得到浮点数的结果,两个运算数必须⾄少有⼀个浮点数,这时 C 语⾔就会进⾏浮点数除法。

int main()
{
  float x = 6 / 4;
  int y = 6 / 4;
  float z = 6.0 / 4;
  printf("%f\n", x);
  printf("%d\n", y);
  printf("%f", z);
  return 0;
}

%算术运算符

它会返回两个整数相除的余(只适用于整数)

负数求模的规则是,结果的正负号由第⼀个运算数的正负号决定:

自增运算符

前置++:先加一后使用

后置++:先使用后加一

①a起始为10,后面a在经历++a后先加一后赋值给b,此时b=11 a=11

②a++时,a的值被直接赋值给了c,然后a再加一,此时c=11 a=12

自减运算符

前置--:先减一后使用

后置--:先使用后减一

①a起始为10,后面a再经历--a后先减一后赋值给b,此时b=9 a=9

②a--时,a的值被直接赋值给了c,然后a再减一,此时c=9 a=8

关系运算符

关系运算符包括:==、!=、>、<、>=、<=

主要作用就是检查两个操作数值是否等于、大于等于之类的,如果是则返回条件为真,如果不是则返回条件为假

逻辑运算符

&&和||为双目运算符操作数 逻辑运算符 操作数

!为单目操作符:!操作数

运算符 描述
&& 同真为真
|| 一真为真
! 逆转真假

非零值为真,零为假

短路

       C语⾔逻辑运算符还有⼀个特点,它总是先对左侧的表达式求值,再对右边的表达式求值,这个顺序是一定的。如果左边的表达式满⾜逻辑运算符的条件,就不再对右边的表达式求值。这种情况称为“短路”。

位运算符

左移位操作符:<<

箭头朝向:向左

运算方法:该数的二进制形式

移位规则:左边抛弃,右边补0

int num = 10;       00000000  00000000  00000000  00001010  num的二进制表示
int a = num << 1;   00000000  00000000  00000000  00010100  a的二进制表示
//移位后num的值不发生改变

右移位操作符:>>

箭头朝向:向右

运算方法:该数的二进制形式  

移位规则:①逻辑右移:左边用0填充,右边丢弃

                 ②算术右移:左边用原该值的符号位填充,右边丢弃

//逻辑右移:
int num = -1;       11111111  11111111  11111111  11111111  num的二进制表示
int a = num >> 1;   01111111  11111111  11111111  11111111  a的二进制表示
//算术右移:
int num = -1;       11111111  11111111  11111111  11111111  num的二进制表示
int a = num >> 1;  1 1111111  11111111  11111111  11111111  a的二进制表示
                  //这个被隔开的1就是符号位,末尾其实已经丢弃一个1了   
//移位后num的值不发生改变

tips:

       对于一个整数,进行左移时结果为原值的两倍,右移时结果为原值的一半,当然,当右移时结果位原值一半时,若原值位奇数那么就向下取整

按位与:&

内存中的运算方式:补码

运算规则:同1为1,其余为0

int a = 3;  
int b = -5; 
int c = a & b; 
// 00000000 00000000 00000000 00000011   3的补码 
// 11111111 11111111 11111111 11111011   -5的补码
// 00000000 00000000 00000000 00000011   c的补码
printf("%d",c);   // c = 3

按位或:|

内存中的运算方式:补码

运算规则:同0为0,其余为1

int a = 3;  
int b = -5; 
int c = a | b; 
// 00000000 00000000 00000000 00000011   3的补码 
// 11111111 11111111 11111111 11111011   -5的补码
// 11111111 11111111 11111111 11111011  c的补码
//负数的补码字符为固定为1,正数的补码字符为固定为0,所以c为负数
// 11111111 11111111 11111111 11111010   c的反码,负数的补码=反码-1
// 10000000 00000000 00000000 00000101   c的原码,负数的原码=反码各个位取反
printf("%d",c);   // c = -5

按位异或:^

内存中的运算方式:补码

运算规则:相同为0,不同为1    

int a = 3;  
int b = -5; 
int c = a ^ b; 
// 00000000 00000000 00000000 00000011   3的补码 
// 11111111 11111111 11111111 11111011   -5的补码
// 11111111 11111111 11111111 11111000   c的补码
//负数的补码字符为固定为1,正数的补码字符为固定为0,所以c为负数
// 11111111 11111111 11111111 11110111  反码,负数的补码=反码-1
// 10000000 00000000 00000000 00001000  原码,负数的原码=反码各个位取反
printf("%d",c);   // c = -8

按位取反操作符:~

内存中的运算方式:补码

运算规则:二进制是0变为1,二进制是1变为0,符号位是+变为-

int main()
{
int n = 0;
int a = ~n;   //按位取反
printf("%d\n",a);
// 00000000  00000000  00000000  00000000  n的二进制形式
// 11111111  11111111  11111111  11111111  a的二进制形式
}

注意:它们的操作数必须为整数!!!

操作数  ^  操作数

操作数  & 操作数

操作数   |  操作数

赋值运算符

赋值运算符包括:=、+=、-=、*=、%=、<<=、>>=、&=、^=、|=、/=、

除了=号是直接赋值以外,其余的比如+=:

a += 3   等价于  a = a + 3

杂项运算符

sizeof关键字(简述)

返回变量大小

取地址操作符(简述)

获取变量地址

解引用操作符*(简述)

获取变量再内存空间中存储的值

条件表达式(? :)

使用形式为:a>b?a:b,如果a大于b则返回值为a,否则为b

操作符的属性:优先级、结合性

       c语言的操作符有两个重要的属性:优先级和结合性,这两个属性在一定程度上决定了表达式求值的计算顺序

1、优先级

优先级是指:如果⼀个表达式包含多个运算符,哪个运算符应该优先执⾏,哪个应该后执行

2、结合性

如果两个运算符优先级相同,优先级没办法确定先计算哪个了,这时候就看结合性了,则根据运算符是左结合,还是右结合。⼤部分运算符是左结合(从左到右执⾏),少数运算符是右结合(从右到左执⾏),⽐如赋值运算符( = )。

 5 * 6 / 2

上⾯⽰例中, * 和 / 的优先级相同,它们都是左结合运算符,所以从左到右进行计算

圆括号的优先级最⾼,可以使⽤它改变其他运算符的优先级

3、运算符优先级和结合性关系表

C语言循环、判断与跳转

循环语句:

for语句

格式:

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

使用规则:表达式1用于循环变量的初始化,表达式2用于循环结束条件的判断,表达式3用于循环变量的调整

注意事项:

for循环中的break和continue

结论:break终止的是它的上一层循环而不是全部的循环

结论:continue会使代码重新回到上一次循环处,且continue后的语句不再执行

while语句

格式:

while(表达式)

       语句

使用规则:表达式结果为真时进入循环执行语句,表达式结果为假时跳出循环

注意事项:先判断后执行

while循环中的break和continue

结论:break终止的是它的上一层循环而不是全部的循环

结论:continue会使代码重新回到上一次循环处,且continue后的语句不再执行

do...while语句

格式:

do

       语句

while(表达式)

使用规则:先执行后判断

注意事项:一般情况下语句总比表达式多运行一次

do...while循环中的continue和break与while循环中的作用一致

判断语句:

if语句

格式:

if(表达式)

       语句

使用规则:表达式为真,语句执行;表达式为假,语句不执行(0为假,非0为真)

注意事项:if只能控制与它最近的那条语句,想要执行多条语句需要加上大括号

if...else语句

格式:

if(表达式)

       语句1
else

       语句2

使用规则:表达式为真执行语句1,表达式为假执行语句2

注意事项:if只能控制与它最近的那条语句,else只能控制与它最近的那条语句(想多条加括号)

if嵌套语句

格式:

if(表达式)

       语句1
else if(表达式)

       语句2

else        

       语句3

使用规则:与上述规则类似

注意事项:注意悬空else问题

解决悬空else问题的关键就是要明确:else总是与离它最近的那个if进行配对,合理使用大括号也可以有效避免该问题

输出为空就是出现了悬空else问题,实际上这段代码是这样的:

当a==1为假时,if中的语句不会执行

switch语句

格式:

switch(整型表达式)

{

case 整型常量表达式1:

                                       语句1;

                                       break;

case 整型常量表达式2:

                                       语句2;

                                       break;

default:

               语句3;

}

使用规则:根据整型表达式不同的值,执行相应的case中的语句,如果找不到对应的值就执行default语句

注意事项:

  1. 牢记这里的表达式都是整型整型表达式或者说是整型常量表达式(比如1)
  2. case和后面的整型常量表达式之间必须有空格
  3. 每一个case语句中的代码执行完成后,需要加上break才能跳出switch语句,但是不是所有情况下都需要加上bradk的

输入1~7,输出对应的星期数:

输入1~7,输出工作日和休息日,其中周一到周五为工作日,周六周日为休息日:

~break的使用要以实际目标为准 ~

最后一点:当整型表达式没有与其配对的整型常量表达式时,就会执行default后的语句,且default的位置是随意的

跳转语句goto

       C语⾔提供了⼀种⾮常特别的语法,就是 goto 语句跳转标号goto 语句可以实现在同⼀个函数 内跳转到设置好的标号处

但是如果goto 语句如果使⽤的不当,就会导致在函数内部随意乱跳转,打乱程序的执⾏流程,所以能不⽤用尽量不去使⽤;但是 goto 语句也不是⼀⽆是处,在多层循环的代码中,如果想快速跳出使⽤ goto 就⾮常的⽅便了。

for(...)

{

       for(...)

       {

               for(...)

               {

                       if(disaster)

                               gotoerror;

               }

       }

}

error:

//...

本来 for 循环想提前退出得使⽤ break ,⼀个 break 只能跳出⼀层 for 循环,如果3层循环嵌套

就得使⽤3个 break 才能跳出循环,所以在这种情况下我们使⽤ goto 语句就会更加的快捷。

C语言函数

函数的概念

库函数

       C语言中的函数就是一个完成某项特定任务的一小段代码,这段代码是有特殊的写法和条用方法的,C语言的程序是由无数个小函数组合而成的,也就是说:⼀个⼤的计算任务可以分解成若⼲个较⼩的函数(对应较⼩的任务)完成。同时⼀个函数如果能完成某项特定任务的话,这个函数也是可以复⽤的,提升了开发软件的效率在C语⾔中我们⼀般会⻅到两类函数:

库函数

⾃定义函数

库函数

基本概念:C语⾔标准中规定了C语⾔的各种语法规则,C语⾔并不提供库函数;C语⾔的国际标准ANSI C规定了⼀些常⽤的函数的标准,被称为标准库,不同的编译器⼚商根据ANSI提供的C语⾔标准就给出了⼀系列函数的实现。这些函数就被称为库函数。

注意事项:使用库函数必须要声明包含该库函数的头文件

库函数学习链接:https://cplusplus.com/

在该链接中可以查看库函数的以下几项内容:

  • 函数原型
  • 函数功能介绍
  • 参数和返回类型说明
  • 代码举例
  • 代码输出
  • 相关知识链接

举例:sqrt函数的查看和使用

#include <stdio.h>
#include <math.h>  //使用该类函数需要包含math.h头文件
int main()
{
 double d = 16.0;
 double r = sqrt(d);
 printf("%lf\n", r);
 return 0;
}

⾃定义函数

⾃定义函数更加重要,能给代码提供更多的创造性

格式:

返回类型 函数名(形参)

{

       //函数体

}

#include <stdio.h>
//实现相加函数
int Add(int x, int y)
{
 return x + y;
}
int main()
{
 int a = 0;
 int b = 0;
 //输⼊
 scanf("%d %d", &a, &b);
 //调⽤加法函数,完成a和b的相加
 //求和的结果放在r中
 int r = Add(a, b);
 //输出
 printf("%d\n", r);
 return 0;
}

形参和实参

在上述内容中int r = Add(a,b);中的a和b就是实参,int Add(int x, int y)中的x和y就是形参。

实参即实际参数:实际参数就是真实传递给函数的参数。

形参即形式参数:如果只是定义了 Add 函数,⽽不去调⽤的话Add 函数的参数 x和 y 只是形式上存在的,不会向内存申请空间,只有在函数被调⽤的过程中为了存放实参传递过来的值,才向内存申请空间。

实参和形参的关系:

#include <stdio.h>
int Add(int x, int y)
{
  int z = 0;
  z = x + y;
  return z;
}
int main()
{
  int a = 0;
  int b = 0;
  //输⼊
  scanf_s("%d %d", &a, &b);
  //调⽤加法函数,完成a和b的相加
  //求和的结果放在r中
  int r = Add(a, b);
  //输出
  printf("%d\n", r);
  return 0;
}

可以发现x和y虽然将a和b的值成功拷贝过去,但是a、b与x、y的地址并不相同所以:

形参是实参的一个临时拷贝,对形参的修改不会影响实参

return语句

在函数的设计中,函数中经常会出现return语句,这⾥讲⼀下return语句使⽤的注意事项:

  • return后可以为数值或者表达式,如果是表达式则先执⾏表达式,再返回表达式结果
  • 可以直接写 return而不跟任何东西, 这种写法适合函数返回类型是void的情况
  • return返回值的类型与函数规定声明的返回类型不⼀致,系统⾃动将return返回的值隐式转换为函数的返回类型
  • return语句执⾏后,函数就彻底返回,后边的代码不再执⾏
  • 如果函数中存在if等分⽀的语句,则要保证每种情况下都有return返回,否则出错

数组做函数参数

数组传参的规则:

  • 函数的形式参数要和函数的实参个数匹配
  • 函数的实参是数组,形参也是可以写成数组形式的(也可以写成指针形式)
  • 形参如果是⼀维数组,数组⼤⼩可以省略不写
  • 形参如果是⼆维数组,⾏可以省略,但是列不能省略
  • 数组传参,形参是不会创建新的数组的
  • 形参操作的数组和实参的数组是同⼀个数组
#include <stdio.h>
void set_arr(int arr[], int sz)
{
  int i = 0;
  for (i = 0; i < sz; i++)
  {
    arr[i] = -1;
  }
}
void print_arr(int arr[], int sz)
{
  int i = 0;
  for (i = 0; i < sz; i++)
  {
    printf("%d ", arr[i]);
  }
  printf("\n");
}
#include <stdio.h>
int main()
{
  int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
  int sz = sizeof(arr) / sizeof(arr[0]);
  set_arr(arr, sz);//设置数组内容为-1,传递数组的首元素地址和数组元素个数
  print_arr(arr, sz);//打印数组内容
  return 0;
}

嵌套调⽤和链式访问

嵌套调用:

#include <stdio.h>
int is_leap_year(int y)
{
  if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
    return 1;
  else
    return 0;
}
int get_days_of_month(int y, int m)
{
  int days[] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
  int day = days[m];
  if (is_leap_year(y) && m == 2)
    day += 1;
  return day;
}
int main()
{
  int y = 0;
  int m = 0;
  scanf_s("%d %d", &y, &m);
  int d = get_days_of_month(y, m);
  printf("%d\n", d);
  return 0;
}

在上述代码中:

  • main 函数调⽤ scanf printf get_days_of_month这些函数
  • get_days_of_month 函数调⽤ is_leap_year函数

实际应用中稍微⼤⼀些代码都是函数之间的嵌套调⽤,但是函数是不能嵌套定义(C语言中)

       函数的嵌套定义是指在一个函数内部定义另一个函数。在某些编程语言中,函数的嵌套定义可能会导致以下错误:

  1. 作用域错误:函数嵌套定义可能引起作用域的混乱。内部函数可以访问外部函数的变量,但外部函数无法访问内部函数的变量。如果在内部函数中引用了外部函数中未定义的变量,或者在外部函数中引用了内部函数的变量,就会导致作用域错误。
  2. 重复定义错误:如果在同一个作用域内定义了两个同名的函数,就会导致重复定义错误。编译器或解释器无法确定应该使用哪个函数,从而引发错误。
  3. 递归调用问题:函数嵌套定义可能导致递归调用问题。如果内部函数递归地调用了外部函数或者自身,而没有正确的终止条件,就会导致无限递归,最终导致栈溢出或程序崩溃。
  4. 编译错误:某些编程语言不支持函数的嵌套定义,因此在这些语言中尝试进行函数嵌套定义会导致编译错误。

       需要注意的是,函数的嵌套定义并不是所有编程语言都支持的特性,具体是否允许函数嵌套定义取决于所使用的编程语言和编译器/解释器的支持。在使用函数嵌套定义时,应仔细阅读语言规范或文档,确保遵循正确的语法和规则。

反正就记住在C语言中不要进行函数的嵌套定义就行

链式访问:

       链式访问就是将⼀个函数的返回值作为另外⼀个函数的参数,像链条⼀样将函数串起来就是函数的链式访问。

#include <stdio.h>
int main()
{
 printf("%d\n", strlen("abcdef"));//链式访问
 return 0;
}

这里将strlen函数的返回值作为printf函数的参数了

函数的声明和定义

函数一定要先声明后使用

单文件:

#include <stdio.h>
//函数声明
int is_leap_year(int y);
int main()
{
  int y = 0;
  scanf_s("%d", &y);
    //函数调用
  int r = is_leap_year(y);  
  if (r == 1)
    printf("闰年\n");
  else
    printf("⾮闰年\n");
  return 0;
}
//函数的定义
int is_leap_year(int y)
{
  if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
    return 1;
  else
    return 0;
}

当然,函数的定义和声明可以放在一起执行

多文件:

⼀般情况下,函数的声明和类型的声明放在头⽂件.h中,函数的实现是放在原⽂件.c⽂件中

add.c文件:

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

add.h文件:

//函数的声明
int Add(int x, int y);

test.c文件:

#include <stdio.h>
#include "add.h"
int main()
{
 int a = 10;
 int b = 20;
 //函数调⽤
 int c = Add(a, b);
 printf("%d\n", c);
 return 0;
}

C语言数组

数组的概念

  • 数组是一组相同类型元素的集合
  • 数组中存放的是一个或者多个数据,但是数类型是组元素个数不能为0
  • 数组中存放的多个数据,类型是相同的
  • 数组分为一维数组和多维数组,其中多维数组一般多见的是二维数组

⼀维数组的创建和初始化

格式:数组类型  数组名[常量值]

存放在数组中的值称为数组元素,数组在创建的时候可以指定数组大小和数组的元素类型

数组的多种初始化方式:

//完全初始化:int arr[5] = {1,2,3,4,5};

//不完全初始化:int arr2[6] = {1};  ------第一个元素初始化为1,剩余元素默认初始化为0

//错误的初始化:int arr[3] = {1,2,3,4,}; ------初始化项太多

数组的类型:

int arr1[10] 的类型为int [10]

char ch[5]的类型为char [5]

⼀维数组的使⽤

C语言规定数组是有下标的,下标从0开始,如果数组有n个元素,最后一个元素的下标为n-1

int arr[10] = {1,2,3,4,5,6,7,8,9,10};
数组:1 2 3 4 5 6 7 8 9 10
下标:0 1 2 3 4 5 6 7 8 9

在C中数组提供了一个操作符,下标引用操作符[]

#include <stdio.h>
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9,10}; 
 printf("%d\n", arr[7]);//结果为8
 printf("%d\n", arr[3]);//结果为4
 return 0;
}

数组的打印:

#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 ", arr[i]);
 }
 return 0;
}

数组的输入:

#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++)
 {
 scanf("%d", &arr[i]);
 }
 for(i=0; i<10; i++)
 {
 printf("%d ", arr[i]);
 }
 return 0;
}

⼀维数组在内存中的存储

#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("&arr[%d] = %p\n ", i, &arr[i]);
 }
 return 0;
}

结论:数组在内存中是连续存放的,数组元素随着下标增长,地址也是由大到小增长的

sizeof计算数组元素个数

       sizeof是C语言中的关键字,可以计算类型或者变量大小的,单位为字节,当然它也可以计算数组大小:

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

这里的40为数组占内存空间的总大小,单位是字节

我们可以通过数组总大小/数组中一个元素大小得到数组元素个数~

#include <stido.h>
int main()
{
 int arr[10] = {0};
 int sz = sizeof(arr)/sizeof(arr[0]);
 printf("%d\n", sz);
 return 0;
}

⼆维数组的创建

格式:类型 数组名[行数][列数]

⼆维数组的初始化

//不完全初始化:int arr1[3][5] = {1,2};

//完全初始化: int arr[3][5] = {1,2,3,4,5,  2,3,4,5,6,  3,4,5,6,7};

//按照行初始化:int arr[3][5] = {{1,2},{3,4},{4,5}};

//省略行的初始化:int arr[][3] = {1,2,3};

⼆维数组的使⽤

~二维数组的行和列都是从0开始的~

#include <stdio.h>
int main()
{
 int arr[3][5] = {1,2,3,4,5, 2,3,4,5,6, 3,4,5,6,7};
//等价于int arr[3][5] = {{1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7}}
 printf("%d\n", arr[2][4]);
 return 0;
}

对于这两种定义方式:

  1. 第一种方式使用了嵌套的{}的方式初始化每一行的元素,代码清晰性和可读性更高
  2. 第二种方式将所有元素连续地放在一个{}中使用逗号分隔,虽然简洁,但可读性稍差

二维数组的输入和输出:

#include <stdio.h>
int main()
{
 int arr[3][5] = {1,2,3,4,5, 2,3,4,5,6, 3,4,5,6,7};
 int i = 0;//遍历⾏
 //输⼊
 for(i=0; i<3; i++) //产⽣⾏号
 {
 int j = 0;
 for(j=0; j<5; j++) //产⽣列号
 {
 scanf("%d", &arr[i][j]); //输⼊数据
 }
 }
 //输出
 for(i=0; i<3; i++) //产⽣⾏号
 {
 int j = 0;
 for(j=0; j<5; j++) //产⽣列号
 {
 printf("%d ", arr[i][j]); //输出数据
 }
 printf("\n");
 }
 return 0;
}

C语言中不能使用printf函数直接打印二维数组,可以打印一维数组

⼆维数组在内存中的存储

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

       从输出的结果来看,每⼀⾏内部的每个元素都是相邻的,地址之间相差4个字节,跨⾏位置处的两个元素(如:arr[0][4]和arr[1][0])之间也是差4个字节,所以⼆维数组中的每个元素都是连续存放的。

C99中的变⻓数组

       在C99标准之前,C语⾔在创建数组的时候,数组⼤⼩的指定只能使⽤常量、常量表达式,或者如果我们初始化数据的话,可以省略数组⼤⼩比如:

int arr[3];

int arr[] = {0};

       这样的语法限制,让我们创建数组就不够灵活,有时候数组⼤了浪费空间,有时候数组⼜⼩了不够⽤的。 C99中给⼀个变⻓数组(variable-length array,简称 VLA)的新特性,允许我们可以使⽤变量指定数组⼤⼩。

请看下⾯两段对于变长数组的使用的的代码:

#include <stdio.h>
void printArray(int size, int arr[size]) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}
int main() {
    int a = 3;
    int b = 5;
    int n = a + b;
    int arr[n];
    // 使用数组
    for (int i = 0; i < n; i++) {
        arr[i] = i;
    }
    printArray(n, arr);
    return 0;
}
#include <stdio.h>
int main()
{
int n = 0;
scanf("%d", &n);//根据输⼊数值确定数组的⼤⼩
int arr[n];
int i = 0;
for (i = 0; i < n; i++)
{
scanf("%d", &arr[i]);
}
for (i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
return 0;
}

gcc环境下:

       

       由于当前使用的Visual Studio 2022是Microsoft的最新版本的集成开发环境,它在C语言方面的支持主要是基于C89/C90标准,而不是C99标准。因此变长数组在Visual Studio 2022中不被支持。

注意事项与补充说明:

  1. 变长数组的大小必须是一个非负整数,且在运行时才能确定。
  2. 变长数组的大小不能是一个常量表达式,也不能是一个全局变量。
  3. 变长数组的声明必须在函数内部,而不是在全局范围内。
  4. 变长数组的生命周期与其所在的作用域相同。

关于Visual Studio对C99标准中变长数组的支持情况,以下是一些相关信息:

  1. Visual Studio 2013及更早版本:这些版本的Visual Studio不支持C99标准中的变长数组。
  2. Visual Studio 2015及更新版本:从Visual Studio 2015开始,Microsoft逐渐增加了对C99标准的支持,并引入了对变长数组的部分支持。然而,需要注意的是,这些版本的支持并不完全符合C99标准的要求。
  3. Visual Studio 2019及更高版本:Visual Studio 2019进一步增强了对C99标准的支持,并提供了更完整的变长数组支持。在这些版本中,你应该能够使用变长数组来定义数组的大小。

       需要注意的是,即使是支持变长数组的Visual Studio版本,也可能存在一些限制和差异。因此,在使用变长数组时,建议查阅Visual Studio的文档以获取更详细和准确的信息。

另外,如果你对C99标准的支持有更高要求,你可以考虑使用其他编译器,如GCC(GNU Compiler Collection)或Clang,它们通常提供更好的C99兼容性

~over~

相关文章
|
6月前
|
缓存 运维 安全
【干货】桌面运维当中,我最常见遇到的几个问题!
作为体制内单位的信息化部门,不管大小事凡是涉及到信息化相关的都会来找我们,平常碰到最多的当然是电脑使用方面的了,比如什么C盘满了让我们帮忙清一下,电脑太慢了让我们帮忙看看啥的,一般新来的小伙子们就会被分配去干这些事情,但是由于在大学或者研究生阶段若非兴趣使然其实很难去了解计算机的一些基础运维知识,这里我也整理了自己常用的一些命令和技巧,帮助小伙伴快速入门。这篇文章主要是针对Windows操作系统而言的,因为目前大部分还依然使用的是Windows操作系统哈
【干货】桌面运维当中,我最常见遇到的几个问题!
|
6月前
|
安全 项目管理
一文搞懂需求流程规范的制定方法和落地技巧
随着业务和产品的发展、团队的不断扩大,很多团队都不可避免的会遇到需求流程混乱的问题。虽然有的团队也编写了一些“需求流程规范”的文档,但最终却流于纸面,难以在团队真正落地。如何科学制定并有效落实需求管理规范呢?对此,云效产品经理陈逊进行了非常详细的直播分享,本文是他经验的文字总结。
102223 19
|
6月前
|
缓存 关系型数据库 MySQL
MySQL调优之服务器参数优化实践
MySQL调优之服务器参数优化实践
1062 1
|
6月前
|
移动开发 小程序
微信小程序web-view嵌入uni-app H5页面,通过H5页面跳转企业微信客户聊天窗口如何操作?
微信小程序web-view嵌入uni-app H5页面,通过H5页面跳转企业微信客户聊天窗口如何操作?
|
6月前
|
SQL Java 数据库连接
MyBatisPlus-聚合查询、分组查询及等值查询
MyBatisPlus-聚合查询、分组查询及等值查询
1036 0
|
6月前
|
消息中间件 存储 Kafka
云消息队列 Kafka 版生态谈第一期:无代码转储能力介绍
云消息队列 Kafka 版生态谈第一期:无代码转储能力介绍
121132 36
|
6月前
|
XML 人工智能 测试技术
软件测试/人工智能|详解selenium xpath定位
软件测试/人工智能|详解selenium xpath定位
|
6月前
|
调度
计算机操作系统-第十六天
计算机操作系统-第十六天
|
6月前
|
JSON 中间件 Go
Go 框架 iris 文档(二)
Go 框架 iris 文档(二)
143 0
|
6月前
|
机器学习/深度学习 Unix Shell
Shell编程基础入门(Bash|变量与输入输出重定向2&1)
Shell编程基础入门(Bash|变量与输入输出重定向2&1)
123 0