c语言学习详解

C语言

一. C语言概述

C语言是一种用于和计算机交流的高级语言, 它既具有高级语言的特点,又具有汇编语言的特点
非常接近自然语言
程序的执行效率非常高
C语言是所有编程语言中的经典,很多高级语言都是从C语言中衍生出来的,
例如:C++、C#、Object-C、Java、Go等等
C语言是所有编程语言中的经典,很多著名的系统软件也是C语言编写的
几乎所有的操作系统都是用C语言编写的
几乎所有的计算机底层软件都是用C语言编写的
几乎所有的编辑器都是C语言编写的

二. 第一个C语言程序

2.1函数定义格式

  • 主函数定义的格式:
    • int 代表函数执行之后会返回一个整数类型的值
    • main 代表这个函数的名字叫做main
    • () 代表这是一个函数
    • {} 代表这个程序段的范围
    • return 0; 代表函数执行完之后返回整数0
int main() {
    // insert code here...
    return 0;
}
  • 其它函数定义的格式
    • int 代表函数执行之后会返回一个整数类型的值
    • call 代表这个函数的名字叫做call
    • () 代表这是一个函数
    • {} 代表这个程序段的范围
    • return 0; 代表函数执行完之后返回整数0
int call() {
    return 0;
}

2. 2 如何执行定义好的函数

  • 主函数(main)会由系统自动调用, 但其它函数不会, 所以想要执行其它函数就必须在main函数中手动调用
    • call 代表找到名称叫做call的某个东西
    • () 代表要找到的名称叫call的某个东西是一个函数
    • ; 代表调用函数的语句已经编写完成
    • 所以call();代表找到call函数, 并执行call函数
int main() {
    call();
    return 0;
}

2.3 什么是关键字

  • 关键字,也叫作保留字。是指一些被C语言赋予了特殊含义的单词
  • 关键字特征:
    • 全部都是小写
    • 在开发工具中会显示特殊颜色
  • 关键字注意点:
    • 因为关键字在C语言中有特殊的含义, 所以不能用作变量名、函数名等
  • C语言中一共有32个关键字
1 2 3 4 5 6 7 8
char short int long float double if else
return do while for switch case break continue
default goto sizeor auto register static extem unsigned
signed typedef struct enum union void const volatile

2.4 什么是标识符?

从字面上理解,就是用来标识某些东西的符号,标识的目的就是为了将这些东西区分开来
其实标识符的作用就跟人类的名字差不多,为了区分每个人,就在每个人出生的时候起了个名字
C语言是由函数构成的,一个C程序中可能会有多个函数,为了区分这些函数,就给每一个函数都起了个名称, 这个名称就是标识符
综上所述: 程序员在程序中给函数、变量等起名字就是标识符

2.4.1 标识符命名规则

  • 只能由字母(a~z、 A~Z)、数字、下划线组成
  • 不能包含除下划线以外的其它特殊字符串
  • 不能以数字开头
  • 不能是C语言中的关键字
  • 标识符严格区分大小写, test和Test是两个不同的标识符

2.4.2 标识符命名规范

  • 见名知意,能够提高代码的可读性
  • 驼峰命名,能够提高代码的可读性
  • 驼峰命名法就是当变量名或函数名是由多个单词连接在一起,构成标识符时,第一个单词以小写字母开始;第二个单词的首字母大写.
  • 例如: myFirstName、myLastName这样的变量名称看上去就像驼峰一样此起彼伏

三. 基本数据类型

3.1什么是数据?

  • 生活中无时无刻都在跟数据打交道
    • 例如:人的体重、身高、收入、性别等数据等
  • 在我们使用计算机的过程中,也会接触到各种各样的数据
    • 例如: 文档数据、图片数据、视频数据等

3.2 数据分类

  • 静态的数据
    静态数据是指一些永久性的数据,一般存储在硬盘中。硬盘的存储空间一般都比较大,现在普通计算机的硬盘都有500G左右,因此硬盘中可以存放一些比较大的文件
    存储的时长:计算机关闭之后再开启,这些数据依旧还在,只要你不主动删掉或者硬盘没坏,这些数据永远都在
    哪些是静态数据:静态数据一般是以文件的形式存储在硬盘上,比如文档、照片、视频等。

  • 动态的数据
    动态数据指在程序运行过程中,动态产生的临时数据,一般存储在内存中。内存的存储空间一般都比较小,现在普通计算机的内存只有8G左右,因此要谨慎使用内存,不要占用太多的内存空间
    存储的时长:计算机关闭之后,这些临时数据就会被清除

  • 哪些是动态数据:

    ​ 当运行某个程序(软件)时,整个程序就会被加载到内存中,在程序运行过程中,会产生各种各样的临时数据,这些临时数据都是存储在内存中的。当程序停止运行或者计算机被强制关闭时,这个程序产生的所有临时数据都会被清除。

  • 数据的计量单位

    • 不管是静态还是动态数据,都是0和1组成的
    • 数据越大,包含的0和1就越多
1 B(Byte字节) = 8 bit(位)
// 00000000 就是一个字节
// 111111111 也是一个字节
// 10101010 也是一个字节
// 任意8个0和1的组合都是一个字节
1 KB(KByte) = 1024 B
1 MB = 1024 KB
1 GB = 1024 MB
1 TB = 1024 GB

3.3 C语言数据类型

  • 作为程序员, 我们最关心的是内存中的动态数据,因为我们写的程序就是在内存中运行的
  • 程序在运行过程中会产生各种各样的临时数据,为了方便数据的运算和操作, C语言对这些数据进行了分类, 提供了丰富的数据类型
  • C语言中有4大类数据类型:基本类型、构造类型、指针类型、空类型

3.4常量的类型

  • 整型常量
    十进制整数。例如:666,-120, 0
    八进制整数,八进制形式的常量都以0开头。例如:0123,也就是十进制的83;-011,也就是十进 制的-9
    十六进制整数,十六进制的常量都是以0x开头。例如:0x123,也就是十进制的291
    二进制整数,逢二进一 0b开头。例如: 0b0010,也就是十进制的2

  • 实型常量
    小数形式
    单精度小数:以字母f或字母F结尾。例如:0.0f、1.01f
    双精度小数:十进制小数形式。例如:3.14、 6.66
    默认就是双精度
    可以没有整数位只有小数位。例如: .3、 .6f

  • 指数形式
    以幂的形式表示, 以字母e或字母E后跟一个10为底的幂数
    上过初中的都应该知道科学计数法吧,指数形式的常量就是科学计数法的另一种表 示,比如123000,用科学计数法表示为1.23×10的5次方
    用C语言表示就是1.23e5或1.23E5
    字母e或字母E后面的指数必须为整数
    字母e或字母E前后必须要有数字
    字母e或字母E前后不能有空格

  • 字符常量
    字符型常量都是用’’(单引号)括起来的。例如:‘a’、‘b’、‘c’
    字符常量的单引号中只能有一个字符
    特殊情况: 如果是转义字符,单引号中可以有两个字符。例如:’\n’、’\t’

  • 字符串常量
    字符型常量都是用""(双引号)括起来的。例如:“a”、“abc”、“lnj”
    系统会自动在字符串常量的末尾加一个字符’\0’作为字符串结束标志

  • 自定义常量

3.5 变量

3.5.1定义变量

  • 格式1: 变量类型 变量名称 ;
  • 为什么要定义变量?
    任何变量在使用之前,必须先进行定义, 只有定义了变量才会分配存储空间, 才有空间存储数据
  • 为什么要限定类型?
    用来约束变量所存放数据的类型。一旦给变量指明了类型,那么这个变量就只能存储这种类型的数据
    内存空间极其有限,不同类型的变量占用不同大小的存储空间
  • 为什么要指定变量名称?
    存储数据的空间对于我们没有任何意义, 我们需要的是空间中存储的值
    只有有了名称, 我们才能获取到空间中的值
int a;
float b;
char ch;
  • 格式2:变量类型 变量名称,变量名称;
    • 连续定义, 多个变量之间用逗号(,)号隔开
int a,b,c;
  • 变量名的命名的规范
    • 变量名属于标识符,所以必须严格遵守标识符的命名原则

3.5.2 如何使用变量?

  • 可以利用=号往变量里面存储数据
    • 在C语言中,利用=号往变量里面存储数据, 我们称之为给变量赋值
int value;
value = 998; // 赋值
  • 注意:
    • 这里的=号,并不是数学中的“相等”,而是C语言中的赋值运算符,作用是将右边的整型常量998赋值给左边的整型变量value
    • 赋值的时候,= 号的左侧必须是变量 (10=b,错误)
    • 为了方便阅读代码, 习惯在 = 的两侧 各加上一个 空格

3.5.3变量的初始化

  • C语言中, 变量的第一次赋值,我们称为“初始化”
  • 初始化的两种形式
    • 先定义,后初始化
    • int value; value = 998; // 初始化
    • 定义时同时初始化
    • int a = 10; int b = 4, c = 2;
    • 其它表现形式(不推荐)
int a, b = 10; //部分初始化
int c, d, e;
c = d = e =0;
  • 不初始化里面存储什么?
    • 随机数
    • 上次程序分配的存储空间,存数一些 内容,“垃圾”
    • 系统正在用的一些数据

3.5.4 如何修改变量值?

  • 多次赋值即可
    • 每次赋值都会覆盖原来的值
int i = 10;
i = 20; // 修改变量的值

3.5.5 变量之间的值传递

  • 可以将一个变量存储的值赋值给另一个变量

3.5.6 如何查看变量的值?

  • 使用printf输出一个或多个变量的值
int a=10;
printf("a=%d",a);
  • 输出其它类型变量的值
char a='A';
printf("a=%C",a);

3.5.7 变量的作用域

  • C语言中所有变量都有自己的作用域
  • 变量定义的位置不同,其作用域也不同
  • 按照作用域的范围可分为两种, 即局部变量和全局变量
3.5.7.1 局部变量
  • 局部变量也称为内部变量
  • 局部变量是在代码块内定义的, 其作用域仅限于代码块内, 离开该代码块后无法使用
int main(){
    int i = 998; // 作用域开始
    return 0;// 作用域结束
}

int main(){
    {
        int i = 998; // 作用域开始
    }// 作用域结束
    printf("i = %d\n", i); // 不能使用
    return 0;
}
3.5.7.2 全局变量
  • 全局变量也称为外部变量,它是在代码块外部定义的变量
int i = 666;
int main(){
    printf("i = %d\n", i); // 可以使用
    return 0;
}// 作用域结束
int call(){
    printf("i = %d\n", i); // 可以使用
    return 0;
}
  • 注意点:
    • 同一作用域范围内不能有相同名称的变量
int main(){
    int i = 998; // 作用域开始
    int i = 666; // 报错, 重复定义
    return 0;
}// 作用域结束

int i = 666; 
int i = 998; // 报错, 重复定义
int main(){
    return 0;
}
  • 不同作用域范围内可以有相同名称的变量
int i = 666; 
int main(){
    int i = 998; // 不会报错
    return 0;
}

int main(){
    int i = 998; // 不会报错
    return 0;
}
int call(){
    int i = 666;  // 不会报错
    return 0;
}

3.5.8 变量内存分析

  • 字节和地址

    • 为了更好地理解变量在内存中的存储细节,先来认识一下内存中的“字节”和“地址”
    • 每一个小格子代表一个字节
    • 每个字节都有自己的内存地址
    • 内存地址是连续的
  • 变量存储占用的空间

    • 一个变量所占用的存储空间,和定义变量时声明的类型以及当前编译环境有关
类型 16位 32位 64位
char 1 1 1
int 2 4 4
float 4 4 4
double 8 8 8
short 2 2 2
long 4 4 8
long long 8 8 8
void* 2 4 8

变量存储的过程

  • 根据定义变量时声明的类型和当前编译环境确定需要开辟多大存储空间
  • 在内存中开辟一块存储空间,开辟时从内存地址大的开始开辟(内存寻址从大到小)
  • 将数据保存到已经开辟好的对应内存空间中
int main(){
  int number;
  int value;
  number = 22;
  value = 666;
}

#include <stdio.h>
int main(){
    int number;
    int value;
    number = 22;
    value = 666;
    printf("&number = %p\n", &number); // 0060FEAC
    printf("&value = %p\n", &value);   // 0060FEA8
}

3.5.9 printf函数

printf函数称之为格式输出函数,方法名称的最后一个字母f表示format。其功能是按照用户指定的格式,把指定的数据输出到屏幕上
printf函数的调用格式为:
printf("格式控制字符串",输出项列表 );
例如:printf("a = %d, b = %d",a, b);

  • 非格式字符串原样输出, 格式控制字符串会被输出项列表中的数据替换

  • 注意: 格式控制字符串和输出项在数量和类型上必须一一对应

  • 格式控制字符串

    • 形式: %[标志][输出宽度][.精度][长度]类型
3.5.9.1 类型
  • 格式: printf("a = %类型", a);
  • 类型字符串用以表示输出数据的类型, 其格式符和意义如下所示
类型 含义
d 有符号16进制整型
i 有符号16进制整型
u 无符号10进制整型
o 无符号8进制整型
x 无符号16进制整型
X 无符号16进制整型
f 单、双精度浮点数(默认保留6位小数)
e/E 以指数形式输出单、双精度浮点数
g/G 以最短输出宽度输出单、双精度浮点数
c 字符
s 字符串
p 地址
#include <stdio.h>
int main(){
    int a = 10;
    int b = -10;
    float c = 6.6f;
    double d = 3.1415926;
    double e = 10.10;
    char f = 'a';
    // 有符号整数(可以输出负数)
    printf("a = %d\n", a); // 10
    printf("a = %i\n", a); // 10

    // 无符号整数(不可以输出负数)
    printf("a = %u\n", a); // 10
    printf("b = %u\n", b); // 429496786

    // 无符号八进制整数(不可以输出负数)
    printf("a = %o\n", a); // 12
    printf("b = %o\n", b); // 37777777766

    // 无符号十六进制整数(不可以输出负数)
    printf("a = %x\n", a); // a
    printf("b = %x\n", b); // fffffff6

    // 无符号十六进制整数(不可以输出负数)
    printf("a = %X\n", a); // A
    printf("b = %X\n", b); // FFFFFFF6

    // 单、双精度浮点数(默认保留6位小数)
    printf("c = %f\n", c); // 6.600000
    printf("d = %lf\n", d); // 3.141593

    // 以指数形式输出单、双精度浮点数
    printf("e = %e\n", e); // 1.010000e+001
    printf("e = %E\n", e); // 1.010000E+001
    
    // 以最短输出宽度,输出单、双精度浮点数
    printf("e = %g\n", e); // 10.1
    printf("e = %G\n", e); // 10.1
    
    // 输出字符
    printf("f = %c\n", f); // a
}
3.5.9.2 宽度
  • 格式: printf("a = %[宽度]类型", a);
  • 用十进制整数来指定输出的宽度, 如果实际位数多于指定宽度,则按照实际位数输出, 如果实际位数少于指定宽度则以空格补位
#include <stdio.h>
int main(){
    // 实际位数小于指定宽度
    int a = 1;
    printf("a =|%d|\n", a); // |1|
    printf("a =|%5d|\n", a); // |    1|
    // 实际位数大于指定宽度
    int b = 1234567;
    printf("b =|%d|\n", b); // |1234567|
    printf("b =|%5d|\n", b); // |1234567|
}
3.5.9.3 标志
  • 格式: printf("a = %[标志][宽度]类型", a);
标志 含义
- 左对齐,默认右对齐
+ 当输出值为正数时,在输出值前面加上一个+号,默认不显示
0 右对齐时,用0填充宽度(默认用空格填充)
空格 输出值为正数时,输出值前加上空格,为负数时加上负号
# 对c,s,d,u类型无影响
# 对o类型,在输出时加前缀o
# 对x类型,在输出时加前缀0x
#include <stdio.h>
int main(){
    int a = 1;
    int b = -1;
    // -号标志
    printf("a =|%d|\n", a); // |1|
    printf("a =|%5d|\n", a); // |    1|
    printf("a =|%-5d|\n", a);// |1    |
    // +号标志
    printf("a =|%d|\n", a); // |1|
    printf("a =|%+d|\n", a);// |+1|
    printf("b =|%d|\n", b); // |-1|
    printf("b =|%+d|\n", b);// |-1|
    // 0标志
    printf("a =|%5d|\n", a); // |    1|
    printf("a =|%05d|\n", a); // |00001|
    // 空格标志
    printf("a =|% d|\n", a); // | 1|
    printf("b =|% d|\n", b); // |-1|
    // #号
    int c = 10;
    printf("c = %o\n", c); // 12
    printf("c = %#o\n", c); // 012
    printf("c = %x\n", c); // a
    printf("c = %#x\n", c); // 0xa
}
3.5.9.4 精度
  • 格式: printf("a = %[精度]类型", a);
  • 精度格式符以"."开头, 后面跟上十进制整数, 用于指定需要输出多少位小数, 如果输出位数大于指定的精度, 则删除超出的部分
#include <stdio.h>
int main(){
    double a = 3.1415926;
    printf("a = %.2f\n", a); // 3.14
}
3.5.9.5 动态指定保留小数位数
  • 格式: printf("a = %.*f", a);
#include <stdio.h>
int main(){
    double a = 3.1415926;
    printf("a = %.*f", 2, a); // 3.14
}
3.5.9.6 实型(浮点类型)有效位数问题
  • 对于单精度数,使用%f格式符输出时,仅前6~7位是有效数字
  • 对于双精度数,使用%lf格式符输出时,前15~16位是有效数字
  • 有效位数和精度(保留多少位)不同, 有效位数是指从第一个非零数字开始,误差不超过本数位半个单位的、精确可信的数位
  • 有效位数包含小数点前的非零数位
#include <stdio.h>
int main(){
    //        1234.567871093750000
    float a = 1234.567890123456789;
    //         1234.567890123456900
    double b = 1234.567890123456789;
    printf("a = %.15f\n", a); // 前8位数字是准确的, 后面的都不准确
    printf("b = %.15f\n", b); // 前16位数字是准确的, 后面的都不准确
}

3.5.9.7 长度
  • 格式: printf("a = %[长度]类型", a);
长度 修饰类型 含义
hh d,i,o,u,x 输出char
h d,i,o,u,x 输出 shourt int
l d,i,o,u,x 输出 long int
ll d,i,o,u,x 输出 long long int
#include <stdio.h>
int main(){
    char a = 'a';
    short int b = 123;
    int  c = 123;
    long int d = 123;
    long long int e = 123;
    printf("a = %hhd\n", a); // 97
    printf("b = %hd\n", b); // 123
    printf("c = %d\n", c); // 123
    printf("d = %ld\n", d); // 123
    printf("e = %lld\n", e); // 123
}
3.5.9.8 转义字符
  • 格式: printf("%f%%", 3.1415);
  • %号在格式控制字符串中有特殊含义, 所以想输出%必须添加一个转义字符
#include <stdio.h>
int main(){
    printf("%f%%", 3.1415); // 输出结果3.1415%
}

3.5.10 Scanf函数

  • scanf函数用于接收键盘输入的内容, 是一个阻塞式函数,程序会停在scanf函数出现的地方, 直到接收到数据才会执行后面的代码

  • printf函数的调用格式为:

    • scanf("格式控制字符串", 地址列表);
    • 例如: scanf("%d", &num);
  • 基本用法

    • 地址列表项中只能传入变量地址, 变量地址可以通过&符号+变量名称的形式获取
#include <stdio.h>
int main(){
    int number;
    scanf("%d", &number); // 接收一个整数
    printf("number = %d\n", number); 
}
  • 接收非字符和字符串类型时, 空格、Tab和回车会被忽略
#include <stdio.h>
int main(){
    float num;
    // 例如:输入 Tab 空格 回车 回车 Tab 空格 3.14 , 得到的结果还是3.14
    scanf("%f", &num);
    printf("num = %f\n", num);
}
  • 非格式字符串原样输入, 格式控制字符串会赋值给地址项列表项中的变量
    • 不推荐这种写法
#include <stdio.h>
int main(){
    int number;
    // 用户必须输入number = 数字  , 否则会得到一个意外的值
    scanf("number = %d", &number);
    printf("number = %d\n", number);
}
  • 接收多条数据
    • 格式控制字符串和地址列表项在数量和类型上必须一一对应
    • 非字符和字符串情况下如果没有指定多条数据的分隔符, 可以使用空格或者回车作为分隔符(不推荐这种写法)
    • 非字符和字符串情况下建议明确指定多条数据之间分隔符
#include <stdio.h>
int main(){
    int number;
    scanf("%d", &number);
    printf("number = %d\n", number);
    int value;
    scanf("%d", &value);
    printf("value = %d\n", value);
}


#include <stdio.h>
int main(){
    int number;
    int value;
    // 可以输入 数字 空格 数字, 或者 数字 回车 数字
    scanf("%d%d", &number, &value);
    printf("number = %d\n", number);
    printf("value = %d\n", value);
}


#include <stdio.h>
int main(){
    int number;
    int value;
    // 输入 数字,数字 即可
    scanf("%d,%d", &number, &value);
    printf("number = %d\n", number);
    printf("value = %d\n", value);
}

  • \n是scanf函数的结束符号, 所以格式化字符串中不能出现\n
#include <stdio.h>
int main(){
    int number;
    // 输入完毕之后按下回车无法结束输入
    scanf("%d\n", &number);
    printf("number = %d\n", number);
}

scanf运行原理

  • 系统会将用户输入的内容先放入输入缓冲区
  • scanf方式会从输入缓冲区中逐个取出内容赋值给变量
  • 如果输入缓冲区的内容不为空,scanf会一直从缓冲区中获取,而不要求再次输入
#include <stdio.h>
int main(){
    int num1;
    int num2;
    char ch1;
    scanf("%d%c%d", &num1, &ch1, &num2);
    printf("num1 = %d, ch1 = %c, num2 = %d\n", num1, ch1, num2);
    char ch2;
    int num3;
    scanf("%c%d",&ch2, &num3);
    printf("ch2 = %c, num3 = %d\n", ch2, num3);
}
  • 利用fflush方法清空缓冲区(不是所有平台都能使用)
    格式: fflush(stdin);
    C和C++的标准里从来没有定义过 fflush(stdin)
    MSDN 文档里清除的描述着"fflush on input stream is an extension to the C standard" (fflush 是在标准上扩充的函数, 不是标准函数, 所以不是所有平台都支持)

  • 利用setbuf方法清空缓冲区(所有平台有效)

    ​ 格式: setbuf(stdin, NULL);

#include <stdio.h>
int main(){
    int num1;
    int num2;
    char ch1;
    scanf("%d%c%d", &num1, &ch1, &num2);
    printf("num1 = %d, ch1 = %c, num2 = %d\n", num1, ch1, num2);
    //fflush(stdin); // 清空输入缓存区
    setbuf(stdin, NULL); // 清空输入缓存区
    char ch2;
    int num3;
    scanf("%c%d",&ch2, &num3);
    printf("ch2 = %c, num3 = %d\n", ch2, num3);
}

3.5.11 putchar和getchar

  • putchar: 向屏幕输出一个字符
#include <stdio.h>
int main(){
    char ch = 'a';
    putchar(ch); // 输出a
}
  • getchar: 从键盘获得一个字符
#include <stdio.h>
int main(){
    char ch;
    ch = getchar();// 获取一个字符
    printf("ch = %c\n", ch);
}

3.6 类型转换

强制类型转换(显示转换) 自动类型转换(隐式转换)
(需要转换的类型)(表达式) 1.算数转换;2.赋值转换
  • 强制转换类型(显示转换)
// 将double转换为int
int a = (int)10.5;
  • 算数转换
    • 系统会自动对占用内存较少的类型做一个“自动类型提升”的操作, 先将其转换为当前算数表达式中占用内存高的类型, 然后再参与运算
// 当前表达式用1.0占用8个字节, 2占用4个字节
// 所以会先将整数类型2转换为double类型之后再计算
double b = 1.0 / 2;
  • 赋值转换
// 赋值时左边是什么类型,就会自动将右边转换为什么类型再保存
int a = 10.6;
  • 注意点:
    • 参与计算的是什么类型, 结果就是什么类型
// 结果为0, 因为参与运算的都是整型
double a = (double)(1 / 2);
// 结果为0.5, 因为1被强制转换为了double类型, 2也会被自动提升为double类型
double b = (double)1 / 2;
  • 类型转换并不会影响到原有变量的值
#include <stdio.h>
int main(){
    double d = 3.14;
    int num = (int)d;
    printf("num = %i\n", num); // 3
    printf("d = %lf\n", d); // 3.140000
}

四. 运算符

4.1 运算符的概念

和数学中的运算符一样, C语言中的运算符是告诉程序执行特定算术或逻辑操作的符号

  • 例如告诉程序, 某两个数相加, 相减,相乘等

什么是表达式

  • 表达式就是利用运算符链接在一起的有意义,有结果的语句;
  • 例如: a + b; 就是一个算数表达式, 它的意义是将两个数相加, 两个数相加的结果就是表达式的结果
  • 注意: 表达式一定要有结果

4.2 运算符分类

按照功能划分:

  • 算术运算符
  • 赋值运算符
  • 关系运算符
  • 逻辑运算符
  • 位运算符

按照参与运算的操作数个数划分:

  • 单目运算
    只有一个操作数 如 : i++;
  • 双目运算
    有两个操作数 如 : a + b;
  • 三目运算
    C语言中唯一的一个,也称为问号表达式 如: a>b ? 1 : 0;

4.2.1 运算符的优先级和结合性

早在小学的数学课本中,我们就学习过"从左往右,先乘除后加减,有括号的先算括号里面的", 这句话就蕴含了优先级和结合性的问题

  • C语言中,运算符的运算优先级共分为15 级。1 级最高,15 级最低
  • 在C语言表达式中,不同优先级的运算符, 运算次序按照由高到低执行
  • 在C语言表达式中,相同优先级的运算符, 运算次序按照结合性规定的方向执行


4.2.2 算数运算符

优先级 名称 符号 说明
3 乘法运算符 * 双目,左结合性
3 除法运算符 / 双目,左结合性
3 求余/模运算符 % 双目,左结合性
4 加法运算符 + 双目,左结合性
4 减法运算符 - 双目,左结合性

注意事项:

  • 如果参与运算的两个操作数皆为整数, 那么结果也为整数
  • 如果参与运算的两个操作数其中一个是浮点数, 那么结果一定是浮点数
  • 求余运算符, 本质上就是数学的商和余"中的余数
  • 求余运算符, 参与运算的两个操作数必须都是整数, 不能包含浮点数
  • 求余运算符, 被除数小于除数, 那么结果就是被除数
  • 求余运算符, 运算结果的正负性取决于被除数,跟除数无关, 被除数是正数结果就是正数,被除数是负数结果就是负数
  • 求余运算符, 被除数为0, 结果为0
  • 求余运算符, 除数为0, 没有意义(不要这样写)
#include <stdio.h>
int main(){
    int a = 10;
    int b = 5;
    // 加法
    int result = a + b;
    printf("%i\n", result); // 15
    // 减法
    result = a - b;
    printf("%i\n", result); // 5
    // 乘法
    result = a * b;
    printf("%i\n", result); // 50
    // 除法
    result = a / b;
    printf("%i\n", result); // 2
    
    // 算术运算符的结合性和优先级
    // 结合性: 左结合性, 从左至右
    int c = 50;
    result = a + b + c; // 15 + c;  65;
    printf("%i\n", result);
    
    // 优先级: * / % 大于 + -
    result = a + b * c; // a + 250; 260;
    printf("%i\n", result);
}

#include <stdio.h>
int main(){
    // 整数除以整数, 结果还是整数
    printf("%i\n", 10 / 3); // 3

    // 参与运算的任何一个数是小数, 结果就是小数
    printf("%f\n", 10 / 3.0); // 3.333333
}

#include <stdio.h>
int main(){
    // 10 / 3 商等于3, 余1
    int result = 10 % 3;
    printf("%i\n", result); // 1

    // 左边小于右边, 那么结果就是左边
    result = 2 % 10;
    printf("%i\n", result); // 2

    // 被除数是正数结果就是正数,被除数是负数结果就是负数
    result = 10 % 3;
    printf("%i\n", result); // 1
    result = -10 % 3;
    printf("%i\n", result); // -1
    result = 10 % -3;
    printf("%i\n", result); // 1
}

4.2.3 赋值运算符

优先级 名称 符号 说明
14 赋值运算符 = 双目,右结合
14 除后赋值运算符 /= 双目,右结合
14 乘后赋值运算符 *= 双目,右结合
14 取模后赋值运算符 %= 双目,右结合
14 加后赋值运算符 += 双目,右结合
14 减后赋值运算符 -= 双目,右结合
  • 简单赋值运算符
#include <stdio.h>
int main(){
    // 简单的赋值运算符 =
    // 会将=右边的值赋值给左边
    int a = 10;
    printf("a = %i\n", a); // 10
}

  • 复合赋值运算符
#include <stdio.h>
int main(){
     // 复合赋值运算符 += -= *= /= %=
     // 将变量中的值取出之后进行对应的操作, 操作完毕之后再重新赋值给变量
     int num1 = 10;
     // num1 = num1 + 1; num1 = 10 + 1; num1 = 11;
     num1 += 1;
     printf("num1 = %i\n", num1); // 11
     int num2 = 10;
     // num2 = num2 - 1; num2 = 10 - 1; num2 = 9;
     num2 -= 1;
     printf("num2 = %i\n", num2); // 9
     int num3 = 10;
     // num3 = num3 * 2; num3 = 10 * 2; num3 = 20;
     num3 *= 2;
     printf("num3 = %i\n", num3); // 20
     int num4 = 10;
     // num4 = num4 / 2; num4 = 10 / 2; num4 = 5;
     num4 /= 2;
     printf("num4 = %i\n", num4); // 5
     int num5 = 10;
     // num5 = num5 % 3; num5 = 10 % 3; num5 = 1;
     num5 %= 3;
     printf("num5 = %i\n", num5); // 1
}

  • 结合性与优先级
#include <stdio.h>
int main(){
    int number = 10;
    // 赋值运算符优先级是14, 普通运算符优先级是3和4, 所以先计算普通运算符
    // 普通运算符中乘法优先级是3, 加法是4, 所以先计算乘法
    // number += 1 + 25; number += 26; number = number + 26; number = 36;
    number += 1 + 5 * 5;
    printf("number = %i\n", number); // 36
}

4.2.4 自增自减运算符

  • 在程序设计中,经常遇到“i=i+1”和“i=i-1”这两种极为常用的操作。
  • C语言为这种操作提供了两个更为简洁的运算符,即++和–
优先级 名称 符号 说明
2 自增运算符(后) i++ 单目,左结合
2 自增运算符(前) ++i 单目,右结合
2 自减运算符(后) i- 单目,左结合
2 自减运算符(前) -i 单目,右结合
  • 自增(自减同理)
    • 如果只有单个变量, 无论++写在前面还是后面都会对变量做+1操作
#include <stdio.h>
int main(){
    int number = 10;
    number++;
    printf("number = %i\n", number); // 11
    ++number;
    printf("number = %i\n", number); // 12
}

如果出现在一个表达式中, 那么++写在前面和后面就会有所区别

  • 前缀表达式:++x, --x;其中x表示变量名,先完成变量的自增自减1运算,再用x的值作为表达式的值;即“先变后用”,也就是变量的值先变,再用变量的值参与运算
  • 后缀表达式:x++, x–;先用x的当前值作为表达式的值,再进行自增自减1运算。即“先用后变”,也就是先用变量的值参与运算,变量的值再进行自增自减变化
#include <stdio.h>
int main(){
    int number = 10;
    // ++在后, 先参与表达式运算, 再自增
    // 表达式运算时为: 3 + 10;
    int result = 3 + number++;
    printf("result = %i\n", result); // 13
    printf("number = %i\n", number); // 11
}

#include <stdio.h>
int main(){
    int number = 10;
    // ++在前, 先自增, 再参与表达式运算
    // 表达式运算时为: 3 + 11;
    int result = 3 + ++number;
    printf("result = %i\n", result); // 14
    printf("number = %i\n", number); // 11
}
  • 注意点:
    • 自增、自减运算只能用于单个变量,只要是标准类型的变量,不管是整型、实型,还是字符型变量等,但不能用于表达式或常量
      • 错误用法: ++(a+b); 5++;
    • 企业开发中尽量让++ – 单独出现, 尽量不要和其它运算符混合在一起

4.2.5 sizeof运算符

sizeof可以用来计算一个变量或常量、数据类型所占的内存字节数

  • 标准格式: sizeof(常量 or 变量);

  • sizeof面试题:

    • sizeof()和+=、*=一样是一个复合运算符, 由sizeof和()两个部分组成, 但是代表的是一个整体
    • 所以sizeof不是一个函数, 是一个运算符, 该运算符的优先级是2

4.2.6 逗号运算符

在C语言中逗号“,”也是一种运算符,称为逗号运算符。 其功能是把多个表达式连接起来组成一个表达式,称为逗号表达式
逗号运算符会从左至右依次取出每个表达式的值, 最后整个逗号表达式的值等于最后一个表达式的值

  • 格式: 表达式1,表达式2,… …,表达式n;
  • 例如: int result = a+1,b=3*4;
#include <stdio.h>
int main(){
    int a = 10, b = 20, c;
    // ()优先级高于逗号运算符和赋值运算符, 所以先计算()中的内容
    // c = (11, 21);
    // ()中是一个逗号表达式, 结果是最后一个表达式的值, 所以计算结果为21
    // 将逗号表达式的结果赋值给c, 所以c的结果是21
    c = (a + 1, b + 1);
    printf("c = %i\n", c); // 21
}

4.2.7 关系运算符

  • 为什么要学习关系运算符
    • 默认情况下,我们在程序中写的每一句正确代码都会被执行。但很多时候,我们想在某个条件成立的情况下才执行某一段代码
    • 这种情况的话可以使用条件语句来完成,但是学习条件语句之前,我们先来看一些更基础的知识:如何判断一个条件是否成立

C语言中的真假性

  • 在C语言中,条件成立称为“真”,条件不成立称为“假”,因此,判断条件是否成立,就是判断条件的“真假”
  • 怎么判断真假呢?C语言规定,任何数值都有真假性,任何非0值都为“真”,只有0才为“假”。也就是说,108、-18、4.5、-10.5等都是“真”,0则是“假”

关系运算符的运算结果只有2种:如果条件成立,结果就为1,也就是“真”;如果条件不成立,结果就为0,也就是“假”

优先级 名称 符号 说明
6 大于运算符 > 双目,左结合
6 小于运算符 < 双目,左结合
6 大于等于运算符 >= 双目,左结合
6 小于等于运算符 <= 双目,左结合
7 等于运算符 == 双目,左结合
7 不等于运算符 != 双目,左结合
#include <stdio.h>
int main(){
    int result = 10 > 5;
    printf("result = %i\n", result); // 1
    result = 5 < 10;
    printf("result = %i\n", result); // 1
    result = 5 > 10;
    printf("result = %i\n", result); // 0
    result = 10 >= 10;
    printf("result = %i\n", result); // 1
    result = 10 <= 10;
    printf("result = %i\n", result); // 1
    result = 10 == 10;
    printf("result = %i\n", result); // 1
    result = 10 != 9;
    printf("result = %i\n", result); // 1
}
  • 优先级和结合性
#include <stdio.h>
int main(){
    // == 优先级 小于 >, 所以先计算>
    // result = 10 == 1; result = 0;
    int result = 10 == 5 > 3;
    printf("result = %i\n", result); // 0
}

#include <stdio.h>
int main(){
    // == 和 != 优先级一样, 所以按照结合性
    // 关系运算符是左结合性, 所以从左至右计算
    // result = 0 != 3; result = 1;
    int result = 10 == 5 != 3;
    printf("result = %i\n", result); // 1
}

  • 注意点:
    • 无论是float还是double都有精度问题, 所以一定要避免利用==判断浮点数是否相等
#include <stdio.h>
int main(){
    float a = 0.1;
    float b = a * 10 + 0.00000000001;
    double c = 1.0 + + 0.00000000001;
    printf("b = %f\n", b);
    printf("c = %f\n", c);
    int result = b == c;
    printf("result = %i\n", result); // 0
}

4.2.8 逻辑运算符

优先级 名称 符号 说明
2 逻辑非运算符 ! 单目,右结合
11 逻辑与运算符 && 双目,左结合
12 逻辑或运算符 \\ 双目,左结合
4.2.7.1 逻辑非
  • 格式: ! 条件A;

  • 运算结果: 真变假,假变真

  • 运算过程:

    • 先判断条件A是否成立,如果添加A成立, 那么结果就为0,即“假”;
    • 如果条件A不成立,结果就为1,即“真”
  • 使用注意:

    • 可以多次连续使用逻辑非运算符
    • !!!0;相当于(!(!(!0)));最终结果为1
#include <stdio.h>
int main(){
    // ()优先级高, 先计算()里面的内容
    // 10==10为真, 所以result = !(1);
    // !代表真变假, 假变真,所以结果是假0
    int result = !(10 == 10);
    printf("result = %i\n", result); // 0
}
4.2.7.2 逻辑与
  • 格式: 条件A && 条件B;
  • 运算结果:一假则假

运算过程:

  • 总是先判断"条件A"是否成立
  • 如果"条件A"成立,接着再判断"条件B"是否成立, 如果"条件B"也成立,结果就为1,即“真”
  • 如果"条件A"成立,"条件B"不成立,结果就为0,即“假”
  • 如果"条件A"不成立,不会再去判断"条件B"是否成立, 因为逻辑与只要一个不为真结果都不为真

使用注意:

  • "条件A"为假, "条件B"不会被执行(惰性运算)
#include <stdio.h>
int main(){
    //               真     &&    真
    int result = (10 == 10) && (5 != 1);
    printf("result = %i\n", result); // 1
    //          假     &&    真
    result = (10 == 9) && (5 != 1);
    printf("result = %i\n", result); // 0
    //          真     &&    假
    result = (10 == 10) && (5 != 5);
    printf("result = %i\n", result); // 0
    //          假     &&    假
    result = (10 == 9) && (5 != 5);
    printf("result = %i\n", result); // 0
}

#include <stdio.h>
int main(){
    int a = 10;
    int b = 20;
    // 逻辑与, 前面为假, 不会继续执行后面
    int result = (a == 9) && (++b);
    printf("result = %i\n", result); // 1
    printf("b = %i\n", b); // 20
}
4.2.7.3 逻辑或
  • 格式: 条件A || 条件B;
  • 运算结果:一真则真

运算过程:

  • 总是先判断"条件A"是否成立

  • 如果"条件A"不成立,接着再判断"条件B"是否成立, 如果"条件B"成立,结果就为1,即“真”

  • 如果"条件A"不成立,"条件B"也不成立成立, 结果就为0,即“假”

  • 如果"条件A"成立, 不会再去判断"条件B"是否成立, 因为逻辑或只要一个为真结果都为真

  • 使用注意:

    • "条件A"为真, "条件B"不会被执行(惰性运算)
#include <stdio.h>
int main(){
    //               真     ||    真
    int result = (10 == 10) || (5 != 1);
    printf("result = %i\n", result); // 1
    //          假     ||    真
    result = (10 == 9) || (5 != 1);
    printf("result = %i\n", result); // 1
    //          真     ||    假
    result = (10 == 10) || (5 != 5);
    printf("result = %i\n", result); // 1
    //          假     ||    假
    result = (10 == 9) || (5 != 5);
    printf("result = %i\n", result); // 0
}

#include <stdio.h>
int main(){
    int a = 10;
    int b = 20;
    // 逻辑或, 前面为真, 不会继续执行后面
    int result = (a == 10) || (++b);
    printf("result = %i\n", result); // 1
    printf("b = %i\n", b); // 20
}

4.2.8 三目运算符

  • 三目运算符,它需要3个数据或表达式构成条件表达式

  • 格式: 表达式1?表达式2(结果A):表达式3(结果B)

    • 示例: 考试及格 ? 及格 : 不及格;
  • 求值规则:

    • 如果"表达式1"为真,三目运算符的运算结果为"表达式2"的值(结果A),否则为"表达式3"的值(结果B)
示例:
    int a = 10;
    int b = 20;
    int max = (a > b) ? a : b;
    printf("max = %d", max);
    输出结果: 20
等价于:
    int a = 10;
    int b = 20;
    int max = 0;
    if(a>b){
      max=a;
    }else {
       max=b;
    }
    printf("max = %d", max);

  • 注意点
    • 条件运算符的运算优先级低于关系运算符和算术运算符,但高于赋值符
    • 条件运算符?和:是一个整体,不能分开使用
#include <stdio.h>
int main(){
    int a = 10;
    int b = 5;
    // 先计算 a > b
    // 然后再根据计算结果判定返回a还是b
    // 相当于int max= (a>b) ? a : b;
    int max= a>b ? a : b;
    printf("max = %i\n", max); // 10
}

#include <stdio.h>
int main(){
    int a = 10;
    int b = 5;
    int c = 20;
    int d = 10;
    // 结合性是从右至左, 所以会先计算:后面的内容
    // int res = a>b?a:(c>d?c:d);
    // int res = a>b?a:(20>10?20:10);
    // int res = a>b?a:(20);
    // 然后再计算最终的结果
    // int res = 10>5?10:(20);
    // int res = 10;
    int res = a>b?a:c>d?c:d;
    printf("res = %i\n", res);
}

4.2.9 位运算符

  • 程序中的所有数据在计算机内存中都是以二进制的形式储存的。
  • 位运算就是直接对整数在内存中的二进制位进行操作
  • C语言提供了6个位操作运算符, 这些运算符只能用于整型操作数
符号 名称 运算结果
& 按位与 同1为1
| 按位或 有1为1
^ 按位异或 不同为1
~ 按位取反 0变1,1变0
<< 按位左移 乘以2的n次方
>> 按位右移 除以2的n次方

按位与:

  • 只有对应的两个二进位均为1时,结果位才为1,否则为0
  • 规律: 二进制中,与1相&就保持原位,与0相&就为0
9&5 = 1

 1001
&0101
------
 0001

按位或:

  • 只要对应的二个二进位有一个为1时,结果位就为1,否则为0
9|5 = 13

 1001
|0101
------
 1101

按位异或

  • 当对应的二进位相异(不相同)时,结果为1,否则为0
  • 规律:
    • 相同整数相的结果是0。比如55=0
    • 多个整数相^的结果跟顺序无关。例如: 567=576
    • 同一个数异或另外一个数两次, 结果还是那个数。例如: 577 = 5
9^5 = 12//转换为二进制在进行计算

 1001
^0101
------
 1100

按位取反

  • 各二进位进行取反(0变1,1变0)
~9 =-10
0000 0000 0000 0000 0000 1001 // 取反前
1111 1111 1111 1111 1111 0110 // 取反后

// 根据负数补码得出结果
1111 1111 1111 1111 1111 0110 // 补码
1111 1111 1111 1111 1111 0101 // 反码
1000 0000 0000 0000 0000 1010 // 源码 == -10

按位左移

  • 把整数a的各二进位全部左移n位,高位丢弃,低位补0
    • 由于左移是丢弃最高位,0补最低位,所以符号位也会被丢弃,左移出来的结果值可能会改变正负性
  • 规律: 左移n位其实就是乘以2的n次方
2<<1; //相当于 2 *= 2 // 4
  0010
<<0100

2<<2; //相当于 2 *= 2^2; // 8
  0010
<<1000

按位右移

  • 把整数a的各二进位全部右移n位,保持符号位不变
    • 为正数时, 符号位为0,最高位补0
    • 为负数时,符号位为1,最高位是补0或是补1(取决于编译系统的规定)
  • 规律: 快速计算一个数除以2的n次方
2>>1; //相当于 2 /= 2 // 1
  0010
>>0001
4>>2; //相当于 4 /= 2^2 // 1
  0100
>>0001

五. 控制流

5.1 控制流基本概念

  • 默认情况下程序运行后,系统会按书写顺序从上至下依次执行程序中的每一行代码。但是这并不能满足我们所有的开发需求, 为了方便我们控制程序的运行流程,C语言提供3种流程控制结构,不同的流程控制结构可以实现不同的运行流程。
  • 这3种流程结构分别是顺序结构、选择结构、循环结构

5.2 选择结构

C语言中提供了两大选择结构, 分别是if和switch

5.2.1 选择结构if

  • if第一种形式
    • 表示如果表达式为真,执行语句块1,否则不执行
if(表达式) {
  语句块1;
}
后续语句;

if(age >= 18) {
  printf("开网卡\n");
}
printf("买烟\n");
  • if第二种形式
    • 如果表达式为真,则执行语句块1,否则执行语句块2
    • else不能脱离if单独使用
if(表达式){
  语句块1;
}else{
  语句块2;
}
后续语句;

if(age > 18){
  printf("开网卡\n");
}else{
  printf("喊家长来开\n");
}
printf("买烟\n");

if第三种形式

  • 如果"表达式1"为真,则执行"语句块1",否则判断"表达式2",如果为真执行"语句块2",否则再判断"表达式3",如果真执行"语句块3", 当表达式1、2、3都不满足,会执行最后一个else语句
  • 众多大括号中,只有一个大括号中的内容会被执行
  • 只有前面所有添加都不满足, 才会执行else大括号中的内容
if(表达式1) {
  语句块1;
}else if(表达式2){
  语句块2;
}else if(表达式3){
  语句块3;
}else{
  语句块4;
}
后续语句;

if(age>40){
  printf("给房卡");
}else if(age>25){
  printf("给名片");
}else if(age>18){
   printf("给网卡");
}else{
  printf("给好人卡");
}
printf("买烟\n");
  • if嵌套
    • if中可以继续嵌套if, else中也可以继续嵌套if
if(表达式1){
    语句块1;
   if(表达式2){
      语句块2;
  }
}else{
   if(表达式3){
      语句块3;
  }else{
      语句块4;
  }
}
  • if注意点
    • 任何数值都有真假性
#include <stdio.h>
int main(){
    if(0){
        printf("执行了if");
    }else{
        printf("执行了else"); // 被执行
    }
}
  • 当if else后面只有一条语句时, if else后面的大括号可以省略

  • 当if else后面的大括号被省略时, else会自动和距离最近的一个if匹配

  • 如果if else省略了大括号, 那么后面不能定义变量

  • C语言中分号(;)也是一条语句, 称之为空语句

  • 但凡遇到比较一个变量等于或者不等于某一个常量的时候,把常量写在前面

#include <stdio.h>
int main(){
    int a = 8;
//    if(a = 10){// 错误写法, 但不会报错
    if (10 == a){
      printf("a的值是10\n");
    }else{
     printf("a的值不是10\n");
    }
}

5.2.2 选择结构switch

  • 由于 if else if 还是不够简洁,所以switch 就应运而生了,他跟 if else if 互为补充关系。switch 提供了点的多路选择

格式:

switch(表达式){
    case 常量表达式1:
        语句1;
        break;
    case 常量表达式2:
        语句2; 
        break;
    case 常量表达式n:
        语句n;
        break;
    default:
        语句n+1;
        break;
}
  • 语义:
    • 计算"表达式"的值, 逐个与其后的"常量表达式"值相比较,当"表达式"的值与某个"常量表达式"的值相等时, 即执行其后的语句, 然后跳出switch语句
    • 如果"表达式"的值与所有case后的"常量表达式"均不相同时,则执行default后的语句
#include <stdio.h>

int main() {

    int num = 3;
    switch(num){
    case 1:
        printf("星期一\n");
        break;
    case 2:
        printf("星期二\n");
        break;
    case 3:
        printf("星期三\n");
        break;
    case 4:
        printf("星期四\n");
        break;
    case 5:
        printf("星期五\n");
        break;
    case 6:
        printf("星期六\n");
        break;
    case 7:
        printf("星期日\n");
        break;
    default:
        printf("回火星去\n");
        break;
    }
}
  • switch注意点

    • switch条件表达式的类型必须是整型, 或者可以被提升为整型的值(char、short)
    • case的值只能是常量, 并且还必须是整型, 或者可以被提升为整型的值(char、short)
    • case后面常量表达式的值不能相同
    • case后面要想定义变量,必须给case加上大括号(理由同if)
    • switch中只要任意一个case匹配, 其它所有的case和default都会失效. 所以如果case和default后面没有break就会出现穿透问题
    • switch中default可以省略
    • switch中default的位置不一定要写到最后, 无论放到哪都会等到所有case都不匹配才会执行(穿透问题除外)
  • if和Switch转换

  • 看上去if和switch都可以实现同样的功能, 那么在企业开发中我们什么时候使用if, 什么时候使用switch呢?

    • if else if 针对于范围的多路选择
    • switch 是针对点的多路选择

5.3 循环结构

C语言中提供了三大循环结构, 分别是while、dowhile和for

  • 循环结构是程序中一种很重要的结构。
    • 其特点是,在给定条件成立时,反复执行某程序段, 直到条件不成立为止。
    • 给定的条件称为"循环条件",反复执行的程序段称为"循环体"

5.3.1 循环结构while

格式:

while (  循环控制条件 ) {
    循环体中的语句;
    能够让循环结束的语句;
    ....
}
  • 构成循环结构的几个条件
    • 循环控制条件
      • 循环退出的主要依据,来控制循环到底什么时候退出
    • 循环体
      • 循环的过程中重复执行的代码段
    • 能够让循环结束的语句(递增、递减、真、假等)
      • 能够让循环条件为假的依据,否则退出循环
int count = 0;
while (count < 3) { // 循环控制条件
    printf("发射子弹~哔哔哔哔\n"); // 需要反复执行的语句
    count++; // 能够让循环结束的语句
}
  • while循环执行流程

    • 首先会判定"循环控制条件"是否为真, 如果为假直接跳到循环语句后面
    • 如果"循环控制条件"为真, 执行一次循环体, 然后再次判断"循环控制条件"是否为真, 为真继续执行循环体,为假跳出循环
    • 重复以上操作, 直到"循环控制条件"为假为止
  • while循环注意点

    • 任何数值都有真假性
    • 当while后面只有一条语句时,while后面的大括号可以省略
    • 如果while省略了大括号, 那么后面不能定义变量(理由同if)
    • C语言中分号(;)也是一条语句, 称之为空语句
    • 最简单的死循环:while (1);

5.3.2 循环结构do while

格式:

do {
    循环体中的语句;
    能够让循环结束的语句;
    ....
} while (循环控制条件 );

int count = 0;
do {
   printf("发射子弹~哔哔哔哔\n");
   count++;
}while(count < 10);
  • do-while循环执行流程

    • 首先不管while中的条件是否成立, 都会执行一次"循环体"
    • 执行完一次循环体,接着再次判断while中的条件是否为真, 为真继续执行循环体,为假跳出循环
    • 重复以上操作, 直到"循环控制条件"为假为止
  • while和dowhile应用场景

    • 绝大多数情况下while和dowhile可以互换, 所以能用while就用while
    • 无论如何都需要先执行一次循环体的情况, 才使用dowhile
    • do while 曾一度提议废除,但是他在输入性检查方面还是有点用的

5.3.3 循环结构for

格式:

for(初始化表达式;循环条件表达式;循环后的操作表达式) {
    循环体中的语句;
}

for(int i = 0; i < 10; i++){
    printf("发射子弹~哔哔哔哔\n");
}

for循环执行流程

  • 首先执行"初始化表达式",而且在整个循环过程中,只会执行一次初始化表达式
  • 接着判断"循环条件表达式"是否为真,为真执行循环体中的语句
  • 循环体执行完毕后,接下来会执行"循环后的操作表达式",然后再次判断条件是否为真,为真继续执行循环体,为假跳出循环
  • 重复上述过程,直到条件不成立就结束for循环

for循环注意点:

  • 和while一模一样
  • 最简单的死循环for(;;);

for和while应用场景

  • while能做的for都能做, 所以企业开发中能用for就用for, 因为for更为灵活
  • 而且对比while来说for更节约内存空间

5.3.4 循环嵌套

循环结构的循环体中存在其他的循环结构,我们称之为循环嵌套

  • 注意: 一般循环嵌套不超过三层
  • 外循环执行的次数 * 内循环执行的次数就是内循环总共执行的次数

循环优化

  • 在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少 CPU 跨切循环层的次数

5.4 四大跳转

C语言中提供了四大跳转语句, 分别是return、break、continue、goto

5.4.1 break

立即跳出switch语句或循环

应用场景:

  • switch
  • 循环结构

break注意点:

  • break离开应用范围,存在是没有意义的
if(1) {
  break; // 会报错
}
  • 在多层循环中,一个break语句只向外跳一层
while(1) {
  while(2) {
    break;// 只对while2有效, 不会影响while1
  }
  printf("while1循环体\n");
}
  • break下面不可以有语句,因为执行不到

5.4.2 continue

结束本轮循环,进入下一轮循环

应用场景:

  • 循环结构

continue注意点:

  • continue离开应用范围,存在是没有意义的

5.4.3 goto

  • 这是一个不太值得探讨的话题,goto 会破坏结构化程序设计流程,它将使程序层次不清,且不易读,所以慎用
  • goto 语句,仅能在本函数内实现跳转,不能实现跨函数跳转(短跳转)。但是他在跳出多重循环的时候效率还是蛮高的
#include <stdio.h>
int main(){
    int num = 0;
// loop:是定义的标记
loop:if(num < 10){
        printf("num = %d\n", num);
        num++;
        // goto loop代表跳转到标记的位置
        goto loop;
    }
}

#include <stdio.h>
int main(){
    while (1) {
        while(2){
            goto lnj;
        }
    }
    lnj:printf("跳过了所有循环");
}

5.4.4 return

结束当前函数,将结果返回给调用者

六. 字符的输入输出

6.1 char类型内存存储

6.1.1 char类型基本概念

  • char是C语言中比较灵活的一种数据类型,称为“字符型”
  • char类型变量占1个字节存储空间,共8位
  • 除单个字符以外, C语言的的转义字符也可以利用char类型存储
字符 意义
\b 退格(BS)当前位置向后回退一个字符
\r 回车(CR),将当前位置移至本行开头
\n 换行(LF),将当前位置移至下一行开头
\t 水平制表(HT),跳到下一个 TAB 位置
\0 用于表示字符串的结束标记
\ 代表一个反斜线字符 \
\" 代表一个双引号字符"
\' 代表一个单引号字符’

6.1.2 char型数据存储原理

  • 计算机只能识别0和1, 所以char类型存储数据并不是存储一个字符, 而是将字符转换为0和1之后再存储
  • 正是因为存储字符类型时需要将字符转换为0和1, 所以为了统一, 老美就定义了一个叫做ASCII表的东东
  • ASCII表中定义了每一个字符对应的整数

    char ch1 = 'a'; 
    printf("%i\n", ch1); // 97

    char ch2 = 97;
    printf("%c\n", ch2); // a

char类型注意点

  • char类型占一个字节, 一个中文字符占3字节(unicode表),所有char不可以存储中文
  • 除转义字符以外, 不支持多个字
  • char类型存储字符时会先查找对应的ASCII码值, 存储的是ASCII值, 所以字符6和数字6存储的内容不同

6.2 字符串

6.2.1 字符串的基本概念

字符串是位于双引号中的字符序列

  • 在内存中以“\0”结束,所占字节比实际多一个
6.2.2 字符串的初始化

在C语言中没有专门的字符串变量,通常用一个字符数组来存放一个字符串。

  • 当把一个字符串存入一个数组时,会把结束符‘\0’存入数组,并以此作为该字符串是否结束的标志。
  • 有了‘\0’标志后,就不必再用字符数组 的长度来判断字符串的长度了

初始化:

    char name[9] = "lnj"; //在内存中以“\0”结束, \0ASCII码值是0
    char name1[9] = {'l','n','j','\0'};
    char name2[9] = {'l','n','j',0};
    // 当数组元素个数大于存储字符内容时, 未被初始化的部分默认值是0, 所以下面也可以看做是一个字符串
    char name3[9] = {'l','n','j'};

错误的初始化方式

    //省略元素个数时, 不能省略末尾的\n
    // 不正确地写法,结尾没有\0 ,只是普通的字符数组
    char name4[] = {'l','n','j'};

     //   "中间不能包含\0", 因为\0是字符串的结束标志
     //    \0的作用:字符串结束的标志
    char name[] = "c\0ool";
     printf("name = %s\n",name);
输出结果: c
6.2.3 字符串输入输出

如果字符数组中存储的是一个字符串, 那么字符数组的输入输出将变得简单方便。

  • 不必使用循环语句逐个地输入输出每个字符
  • 可以使用printf函数和scanf函数一次性输出输入一个字符数组中的字符串

使用的格式字符串为“%s”,表示输入、输出的是一个字符串 字符串的输出

输出

  • %s的本质就是根据传入的name的地址逐个去取数组中的元素然后输出,直到遇到\0位置
char chs[] = "lnj";//常量,不可改变
printf("%s\n", chs);
  • 注意点:
    • \0引发的错读问题
char name[] = {'c', 'o', 'o', 'l' , '\0'};
char name2[] = {'l', 'n', 'j'};
printf("name2 = %s\n", name2); // 输出结果: lnjcool

输入

char ch[10];
scanf("%s",ch);

注意点:

  • 对一个字符串数组, 如果不做初始化赋值, 必须指定数组长度
  • ch最多存放由9个字符构成的字符串,其中最后一个字符的位置要留给字符串的结尾标示‘\0’
  • 当用scanf函数输入字符串时,字符串中不能含有空格,否则将以空格作为串的结束符
6.2.4 字符串常用方法

C语言中供了丰富的字符串处理函数,大致可分为字符串的输入、输出、合并、修改、比较、转 换、复制、搜索几类。

  • 使用这些函数可大大减轻编程的负担。
  • 使用输入输出的字符串函数,在使用前应包含头文件"stdio.h"
  • 使用其它字符串函数则应包含头文件"string.h"
6.2.4.1 字符串输出函数:puts
  • 格式: puts(字符数组名)

  • 功能:把字符数组中的字符串输出到显示器。即在屏幕上显示该字符串。

  • 优点:

    • 自动换行
    • 可以是数组的任意元素地址
  • 缺点

    • 不能自定义输出格式, 例如 puts(“hello %i”);
char ch[] = "lnj";
puts(ch); //输出结果: lnj

注:puts函数完全可以由printf函数取代。当需要按一定格式输出时,通常使用printf函数

6.2.4.2 字符串输入函数:gets/fgets
  • 格式: gets (字符数组名)
  • 功能:从标准输入设备键盘上输入一个字符串。
char ch[30];
gets(ch); // 输入:lnj
puts(ch); // 输出:lnj
  • 可以看出当输入的字符串中含有空格时,输出仍为全部字符串。说明gets函数并不以空格作为字符串输入结束的标志,而只以回车作为输入结束。这是与scanf函数不同的。
  • 注意gets很容易导致数组下标越界,是一个不安全的字符串操作函数

格式:fgets(char *s,int size,FILE *stream);

所需头文件:#include <stdio.h>

参数:

  • s(字符串首地址);
  • size(指定最大读取字符串的长度);
  • stream(文件指针,如果读取键盘输入的自负床,固定写为stdin);
char a[10];
fgets(a,2,stdio);
printf("%s",a);

回车也会读入字符串

6.2.4.3 字符串长度
  • 利用sizeof字符串长度
    • 因为字符串在内存中是逐个字符存储的,一个字符占用一个字节,所以字符串的结束符长度也是占用的内存单元的字节数。
char name[] = "it666";
int size = sizeof(name);// 包含\0
printf("size = %d\n", size); //输出结果:6

利用系统函数

  • 格式: strlen(字符数组名)//需要string.h
  • 功能:测字符串的实际长度(不含字符串结束标志‘\0’)并作为函数返回值。
    char name[] = "it666";
    size_t len = strlen(name2);
    printf("len = %lu\n", len); //输出结果:5

以“\0”为字符串结束条件进行统计

/**
 *  自定义方法计算字符串的长度
 *  @param name 需要计算的字符串
 *  @return 不包含\0的长度
 */
int myStrlen2(char str[])
{
    //    1.定义变量保存字符串的长度
    int length = 0;
    while (str[length] != '\0')
    {
        length++;//1 2 3 4
    }
    return length;
}
/**
 *  自定义方法计算字符串的长度
 *  @param name  需要计算的字符串
 *  @param count 字符串的总长度
 *  @return 不包含\0的长度
 */
int myStrlen(char str[], int count)
{
//    1.定义变量保存字符串的长度
    int length = 0;
//    2.通过遍历取出字符串中的所有字符逐个比较
    for (int i = 0; i < count; i++) {
//        3.判断是否是字符串结尾
        if (str[i] == '\0') {
            return length;
        }
        length++;
    }
    return length;
}
6.2.4.4 字符串连接函数:strcat/strncat
  • 格式: strcat(字符数组名1,字符数组名2)
    • strncat(字符数组1,字符数组2,最多可链接字节数)
  • 功能:把字符数组2中的字符串连接到字符数组1 中字符串的后面,并删去字符串1后的串标志 “\0”。本函数返回值是字符数组1的首地址
char oldStr[100] = "welcome to";
char newStr[20] = " lnj";
strcat(oldStr, newStr);
puts(oldStr); //输出: welcome to lnj"
  • 本程序把初始化赋值的字符数组与动态赋值的字符串连接起来。要注意的是,字符数组1应定义足 够的长度,否则不能全部装入被连接的字符串。
6.2.4.5 字符串拷贝函数:strcpy/strncpy
  • 格式: strcpy(字符数组名1,字符数组名2)
    • strncpy(字符数组1,字符数组2,最多可复制字节数)
  • 功能:把字符数组2中的字符串拷贝到字符数组1中。串结束标志“\0”也一同拷贝。字符数名2, 也可以是一个字符串常量。这时相当于把一个字符串赋予一个字符数组。
char oldStr[100] = "welcome to";
char newStr[50] = " lnj";
strcpy(oldStr, newStr);
puts(oldStr); // 输出结果:  lnj // 原有数据会被覆盖
  • 本函数要求字符数组1应有足够的长度,否则不能全部装入所拷贝的字符串。
6.2.4.6 字符串比较函数:strcmp/strncmp
  • 格式: strcmp(字符数组名1,字符数组名2)
    • stncmp(字符数组1,字符数组2,比较前n个字节)
  • 功能:按照ASCII码顺序比较两个数组中的字符串,并由函数返回值返回比较结果。
    • 字符串1=字符串2,返回值=0;
    • 字符串1>字符串2,返回值>0;
    • 字符串1<字符串2,返回值<0。
    char oldStr[100] = "0";
    char newStr[50] = "1";
    printf("%d", strcmp(oldStr, newStr)); //输出结果:-1
    char oldStr[100] = "1";
    char newStr[50] = "1";
    printf("%d", strcmp(oldStr, newStr));  //输出结果:0
    char oldStr[100] = "1";
    char newStr[50] = "0";
    printf("%d", strcmp(oldStr, newStr)); //输出结果:1

6.2.4.7 字符串寻找函数 strstr
  • 功能:在指定的字符串中,找到一个子串
  • 头文件:#include<string.h>
  • 格式 :char *strstr(指定字符串,需要查询的子串);
  • 返回值:
    • 成功:指向子串的指针;
    • 失败:NULL;
char *s = "abcde.txt";
char *p = strstr(s,".wps");
if(p=NULL)
{
    printf("%s非wps文件\n",s);
}
else
    printf("%s是wps文件",s);
6.2.4.8 字符串拆解函数strtok()
  • 功能:将某个字符串,按照指定的分隔符拆解为子串;

  • 头文件:#include <string.h>

  • 格式:char *strtok(指定字符串,分隔符(可指定多个分隔符))

  • 返回值:

    • 成功:指向子串的指针;
    • 失败:NULL;
  • 注:

  • 该函数会将改变原始字符串 str,使其所包含的所有分隔符变成结束标记 '\0'。

  • 由于该函数需要更改字符串 str,因此 str 指向的内存必须是可写的。

  • 首次调用时 str 指向原始字符串,此后每次调用 str 用 NULL 代替。

char s[20] = "www.baidu.com";
char *p = strtok(s,".");
while(p!=NULL)
{
    printf("%s\n",p);
    p = strtok(NULL,".");
}
6.2.4.9 查找字符串特定字符函数strchr()/strrchr
  • 功能:在字符串内查找特定字符
  • 头文件:#include <string.h>
  • 格式:strchr(字符串,待查字符)
    • strrchr(字符串,待查字符)
  • 返回值:
    • 成功:返回第一次出现的c地址
    • 失败:NULL
  • 注:
    • 这两个函数的功能,都是在指定的字符串 s 中,试图找到字符 c。
    • strchr() 从左往右找,strrchr() 从右往左找。
    • 字符串结束标记 '\0' 被认为是字符串的一部分。
char *p;
p = strchr("www.qq.com",'.');
printf("%s\n",p);//输出".qq.com"

p = strrchr("www.qq.com",'.');
printf("%s\n",p);//输出".com"
6.2.5 字符串数组基本概念

字符串数组其实就是定义一个数组保存所有的字符串

  • 1.一维字符数组中存放一个字符串,比如一个名字char name[20] = “nj”
  • 2.如果要存储多个字符串,比如一个班所有学生的名字,则需要二维字符数组,char names[15][20]可以存放15个学生的姓名(假设姓名不超过20字符)
  • 如果要存储两个班的学生姓名,那么可以用三维字符数组char names[2][15][20]

字符串数组的初始化

char names[2][10] = { {'l','n','j','\0'}, {'l','y','h','\0'} };
char names2[2][10] = { {"lnj"}, {"lyh"} };
char names3[2][10] = { "lnj", "lyh" };

七. 数组

7.1 一维数组

7.1.1 数组概念

数组,从字面上看,就是一组数据的意思,没错,数组就是用来存储一组数据的
在C语言中,数组属于构造数据类型
(1)数组的几个名词
数组:一组相同数据类型数据的有序的集合
数组元素: 构成数组的每一个数据。
数组的下标: 数组元素位置的索引(从0开始)
(2)数组的应用场景
一个int类型的变量能保存一个人的年龄,如果想保存整个班的年龄呢?
第一种方法是定义很多个int类型的变量来存储
第二种方法是只需要定义一个int类型的数组来存储

#include <stdio.h>

int main(int argc, const char * argv[]) {
    /*
    // 需求: 保存2个人的分数
    int score1 = 99;
    int score2 = 60;
    
    // 需求: 保存全班同学的分数(130人)
    int score3 = 78;
    int score4 = 68;
    ...
    int score130 = 88;
    */
    // 数组: 如果需要保存`一组``相同类型`的数据, 就可以定义一个数组来保存
    // 只要定义好一个数组, 数组内部会给每一块小的存储空间一个编号, 这个编号我们称之为 索引, 索引从0开始
    // 1.定义一个可以保存3个int类型的数组
    int scores[3];
    
    // 2.通过数组的下标往数组中存放数据
    scores[0] = 998;
    scores[1] = 123;
    scores[2] = 567;
   
    // 3.通过数组的下标从数组中取出存放的数据
    printf("%i\n", scores[0]);
    printf("%i\n", scores[1]);
    printf("%i\n", scores[2]);
    return 0;
}

7.1.2 定义数组

元素类型 数组名[元素个数];

// int 元素类型
// ages 数组名称
// [10] 元素个数
int ages[10];

7.1.3 初始化数组

(1)定义的同时初始化
指定元素个数,完全初始化
其中在{ }中的各数据值即为各元素的初值,各值之间用逗号间隔

int ages[3] = {4, 6, 9};

(2)不指定元素个数,完全初始化
根据大括号中的元素的个数来确定数组的元素个数

char ages[] = {4, 6, 9};

(3)指定元素个数,部分初始化
没有显式初始化的元素,那么系统会自动将其初始化为0

int nums[10] = {1,2};

(4)指定元素个数,部分初始化

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

(5)不指定元素个数,部分初始化

int nums[] = {[4] = 3};

(6)先定义后初始化

int nums[3];
nums[0] = 1;
nums[1] = 2;
nums[2] = 3;

(7)没有初始化会怎样?
如果定义数组后,没有初始化,数组中是有值的,是随机的垃圾数,所以如果想要正确使用数组应该要进行初始化。

#include <stdio.h>
int main()
{
    int nums[5];
	printf("%d\n", nums[0]);
	printf("%d\n", nums[1]);
	printf("%d\n", nums[2]);
	printf("%d\n", nums[3]);
	printf("%d\n", nums[4]);
	return 0;
}
/*
输出结果:
4195744
0
4195392
0
-2057246720
*/

注意:

​ 1.使用数组时不能超出数组的索引范围使用, 索引从0开始, 到元素个数-1结束

​ 2.使用数组时不要随意使用未初始化的元素, 有可能是一个随机值

​ 3.对于数组来说, 只能在定义的同时初始化多个值, 不能先定义再初始化多个值

//eg:
int ages[3];
ages = {4, 6, 9}; // 报错

7.1.4 数组的使用

通过下标(索引)访问:

// 找到下标为0的元素, 赋值为10
ages[0]=10;
// 取出下标为2的元素保存的值
int a = ages[2];
printf("a = %d", a);

7.1.5 数组的遍历

数组的遍历:遍历的意思就是有序地查看数组的每一个元素

int ages[4] = {19, 22, 33, 13};
for (int i = 0; i < 4; i++) {
    printf("ages[%d] = %d\n", i, ages[i]);
}

7.1.6 数组长度计算方法

因为数组在内存中占用的字节数取决于其存储的数据类型和数据的个数
数组所占用存储空间 = 一个元素所占用存储空间 * 元素个数(数组长度)
所以计算数组长度可以使用如下方法
数组的长度 = 数组占用的总字节数 / 数组元素占用的字节数

int ages[4] = {19, 22, 33, 13};
int length =  sizeof(ages)/sizeof(int);
printf("length = %d", length);
输出结果: 4

7.1.7 数组内部存储细节

存储方式:

​ 1)内存寻址从大到小, 从高地址开辟一块连续没有被使用的内存给数组
​ 2)从分配的连续存储空间中, 地址小的位置开始给每个元素分配空间
​ 3)从每个元素分配的存储空间中, 地址最大的位置开始存储数据
​ 4)用数组名指向整个存储空间最小的地址

#include <stdio.h>
int main()
{
    int num = 9;
    char cs[] = {'l','n','j'};
    printf("cs = %p\n", &cs);       
    printf("cs[0] = %p\n", &cs[0]); 
    printf("cs[1] = %p\n", &cs[1]); 
    printf("cs[2] = %p\n", &cs[2]); 

    int nums[] = {2,6,8};
    printf("nums = %p\n", &nums);      
    printf("nums[0] = %p\n", &nums[0]);
    printf("nums[1] = %p\n", &nums[1]);
    printf("nums[1] = %p\n", &nums[2]);
    return 0;
}
/*
输出:
cs = 0x7ffc1f865c10
cs[0] = 0x7ffc1f865c10
cs[1] = 0x7ffc1f865c11
cs[2] = 0x7ffc1f865c12
nums = 0x7ffc1f865c20
nums[0] = 0x7ffc1f865c20
nums[1] = 0x7ffc1f865c24
nums[1] = 0x7ffc1f865c28
*/

注:字符在内存中是以对应ASCII码值的二进制形式存储的,而非上述的形式。

7.1.8 数组的越界问题

数组越界导致的问题
(1)约错对象
(2)程序崩溃

    char cs1[2] = {1, 2};
    char cs2[3] = {3, 4, 5};
    cs2[3] = 88; // 注意:这句访问到了不属于cs2的内存

7.1.9 数组注意事项

在定义数组的时候[]里面只能写整型常量或者是返回整型常量的表达式

int ages4['A'] = {19, 22, 33};
printf("ages4[0] = %d\n", ages4[0]);

int ages5[5 + 5] = {19, 22, 33};
printf("ages5[0] = %d\n", ages5[0]);

int ages5['A' + 5] = {19, 22, 33};
printf("ages5[0] = %d\n", ages5[0]);

错误写法

// 没有指定元素个数,错误
int a[];

// []中不能放变量
int number = 10;
int ages[number]; // 老版本的C语言规范不支持
printf("%d\n", ages[4]);

int number = 10;
int ages2[number] = {19, 22, 33} // 直接报错

// 只能在定义数组的时候进行一次性(全部赋值)的初始化
int ages3[5];
ages10 = {19, 22, 33};

// 一个长度为n的数组,最大下标为n-1, 下标范围:0~n-1
int ages4[4] = {19, 22, 33}
ages4[8]; // 数组角标越界

7.2 二维数组

所谓二维数组就是一个一维数组的每个元素又被声明为一 维数组,从而构成二维数组. 可以说二维数组是特殊的一维数组。
示例:
可以看作由一维数组a[0]和一维数组a[1]组成,这两个一维数组都包含了3个int类型的元素

int a[2][3] = { {80,75,92}, {61,65,71}};

7.2.1 二维数组的定义

格式:

  • 数据类型 数组名【一维数组的个数】【一维数组的元素个数】
  • 其中"一维数组的个数"表示当前二维数组中包含多少个一维数组
  • 其中"一维数组的元素个数"表示当前前二维数组中每个一维数组元素的个数

7.2.2 二维数组的初始化

二维数的初始化可分为两种:
(1)定义的同时初始化
(2)先定义后初始化

  • 定义的同时初始化
int a[2][3]={ {80,75,92}, {61,65,71}};
  • 先定义后初始化
int a[2][3];
a[0][0] = 80;
a[0][1] = 75;
a[0][2] = 92;
a[1][0] = 61;
a[1][1] = 65;
a[1][2] = 71;
  • 按行分段赋值
int a[2][3]={ 80,75,92,61,65,71};
  • 按行连续赋值
int a[2][3]={ 80,75,92,61,65,71};

其它写法

  • 完全初始化,可以省略第一维的长度
int a[][3]={{1,2,3},{4,5,6}};
int a[][3]={1,2,3,4,5,6};
  • 部分初始化,可以省略第一维的长度
int a[][3]={{1},{4,5}};
int a[][3]={1,2,3,4};

注: 有些人可能想不明白,为什么可以省略行数,但不可以省略列数。也有人可能会问,可不可以只指定行数,但是省略列数?其实这个问题很简单,如果我们这样写:

int a[2][] = {1, 2, 3, 4, 5, 6}; // 错误写法
  • 指定元素的初始化
int a[2][3]={[1][2]=10};
int a[2][3]={[1]={1,2,3}}

大家都知道,二维数组会先存放第1行的元素,由于不确定列数,也就是不确定第1行要存放多少个元素,所以这里会产生很多种情况,可能1、2是属于第1行的,也可能1、2、3、4是第一行的,甚至1、2、3、4、5、6全部都是属于第1行的

7.2.3 二维数组的遍历

二维数组a[ 3 ][ 4 ] ,可分解为三个一维数组,其数组名分别为:
这三个一维数组都有4个元素,例如:一维数组a[0]的 元素为a[0][0],a[0][1],a[0][2],a[0][3]
所以遍历二维数组无非就是先取出二维数组中得一维数组, 然后再从一维数组中取出每个元素的值

char cs[2][3] = {{'a', 'b', 'c'},{'d', 'e', 'f'}};
	for (int i = 0; i < 2; i++) { // 外循环取出一维数组
        // i
        for (int j = 0; j < 3; j++) {// 内循环取出一维数组的每个元素
            printf("%c", cs[i][j]);
        }
        printf("\n");
    }

注: 必须强调的是,a[0],a[1],a[2]不能当作下标变量使用,它们是数组名,不是一个单纯的下标变量

7.2.4 二维数组的存储

和一维数组一样

  • 给数组分配存储空间从内存地址大开始分配
  • 给数组元素分配空间, 从所占用内存地址小的开始分配
  • 往每个元素中存储数据从高地址开始存储
#include <stdio.h>
int main()
{
    char cs[2][3] = {
        {'a', 'b', 'c'},
        {'d', 'e', 'f'}
    };
    // cs == &cs == &cs[0] == &cs[0][0]
    printf("cs = %p\n", cs);                // 0060FEAA
    printf("&cs = %p\n", &cs);              // 0060FEAA
    printf("&cs[0] = %p\n", &cs[0]);        // 0060FEAA
    printf("&cs[0][0] = %p\n", &cs[0][0]);  // 0060FEAA
    return 0;
}

八. 指针

  • 地址与内存单元中的数据是两个完全不同的概念
    • 地址如同房间编号, 根据这个编号我们可以找到对应的房间
    • 内存单元如同房间, 房间是专门用于存储数据的
  • 变量地址:
    • 系统分配给"变量"的"内存单元"的起始地址
int num = 6; // 占用4个字节
//那么变量num的地址为: 0ff06

char c = 'a'; // 占用1个字节
//那么变量c的地址为:0ff05

  • 什么是指针

在计算机中所有数据都存储在内存单元中,而每个内存单元都有一个对应的地址, 只要通过这个地址就能找到对应单元中存储的数据.
由于通过地址能找到所需的变量单元,所以我们说该地址指向了该变量单元。将地址形象化的称为“指针”
内存单元的指针(地址)和内存单元的内容是两个不同的概念。

  • 什么是指针变量

在C语言中,允许用一个变量来存放其它变量的地址, 这种专门用于存储其它变量地址的变量, 我们称之为指针变量

int age;// 定义一个普通变量
num = 10;
int *pnAge; // 定义一个指针变
pnAge = &age;
  • 定义指针变量的格式

指针变量的定义包括两个内容:

​ (1)指针类型说明,即定义变量为一个指针变量;

​ (2)指针变量名;

char ch = 'a';
char *p; // 一个用于指向字符型变量的指针
p = &ch;  
int num = 666;
int *q; // 一个用于指向整型变量的指针
q = &num;  
  • 其中,*表示这是一个指针变量
  • 变量名即为定义的指针变量名
  • 类型说明符表示本指针变量所指向的变量的数据类型

8.1 指针变量的初始化方法

指针变量初始化的方法有两种:定义的同时进行初始化和先定义后初始化

  • 定义的同时进行初始化
int a = 5;
int *p = &a;
  • 先定义后初始化
int a = 5;
int *p;
p=&a;
  • 把指针初始化为NULL
int *p=NULL;
int *q=0;

不合法的初始化:

  • 指针变量只能存储地址, 不能存储其它类型
int *p;
p =  250; // 错误写法
  • 给指针变量赋值时,指针变量前不能再加“*”
int *p;
*p=&a; //错误写法

注意点:

  • 多个指针变量可以指向同一个地址

  • 指针的指向是可以改变的
int a = 5;
int *p = &a;
int b = 10;
p = &b; // 修改指针指向

指针没有初始化里面是一个垃圾值,这时候TA是一个野指针

  • 野指针可能会导致程序崩溃
  • 野指针访问你不该访问数据
  • 所以指针必须初始化才可以访问其所指向存储区域

8.2 访问指针所指向的存储空间

  • C语言中提供了地址运算符&来表示变量的地址。其一般形式为:
    • &变量名;
  • C语言中提供了*来定义指针变量和访问指针变量指向的内存存储空间
    • 在定义变量的时候 * 是一个类型说明符,说明定义的这个变量是一个指针变量
int *p=NULL; // 定义指针变量
  • 在不是定义变量的时候 *是一个操作符,代表访问指针所指向存储空间
int a = 5;
int *p = &a;
printf("a = %d", *p); // 访问指针变量

malloc函数:
简单来说,malloc函数的作用是开辟一个空间来给你使用;

malloc时动态内存分配函数,用于申请一块连续的指定大小的内存块区域以void*类型返回分配的内存区域地址

malloc函数原型

extern void *malloc(unsigned int num_bytes);
意为分配长度为num_bytes字节的内存块

malloc函数头文件

#include <malloc.h>
#include <string.h>//使用sizeof函数是需要此头文件

malloc函数返回值
如果分配成功则返回指向被分配内存的指针,否则返回空指针NULL。

malloc函数使用注意事项
malloc函数的返回的是无类型指针,在使用时一定要强制转换为所需要的类型。
***重点:在使用malloc开辟空间时,使用完成一定要释放空间,如果不释放会造内存泄漏。
在使用malloc函数开辟的空间中,不要进行指针的移动,因为一旦移动之后可能出现申请的空间和释放空间大小的不匹配

malloc函数使用形式
关于malloc所开辟空间类型:malloc只开辟空间,不进行类型检查,只是在使用的时候进行类型的强转。
举个例子:‘我’开辟你所需要大小的字节大小空间,至于怎么使用是你的事
mallo函数返回的实际是一个无类型指针,必须在其前面加上指针类型强制转换才可以使用
指针自身 = (指针类型)malloc(sizeof(指针类型)数据数量)

int *p = NULL;
int n = 10;
p = (int *)malloc(sizeof(int)*n);

在使用malloc函数之前我们一定要计算字节数,malloc开辟的是用户所需求的字节数大小的空间。
如果多次申请空间那么系统是如何做到空间的不重复使用呢?
在使用malloc开辟一段空间之后,系统会在这段空间之前做一个标记(0或1),当malloc函数开辟空间如果遇到标记为0就在此开辟,如果为1说明此空间正在被使用

8.3 指针类型

在同一种编译器环境下,一个指针变量所占用的内存空间是固定的。
虽然在同一种编译器下, 所有指针占用的内存空间是一样的,但不同类型的变量却占不同的字节数

  • 一个int占用4个字节,一个char占用1个字节,而一个double占用8字节;
  • 现在只有一个地址,我怎么才能知道要从这个地址开始向后访问多少个字节的存储空间呢,是4个,是1个,还是8个。
  • 所以指针变量需要它所指向的数据类型告诉它要访问多少个字节存储空间

从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。让我们看看例一中各个指针的类型:

(1)int *ptr;//指针的类型是int*
(2)char *ptr;//指针的类型是char*
(3)int **ptr;//指针的类型是int**
(4)int (*ptr)[3];//指针的类型是int(*)[3]
(5)int *(*ptr)[4];//指针的类型是int*(*)[4]

复杂类型说明
要了解指针,多多少少会出现一些比较复杂的类型,所以我先介绍一下如何完全理解一个复杂类型,要理解复杂类型其实很简单,一个类型里会出现很多运算符,他们也像普通的表达式一样,有优先级,其优先级和运算优先级一样,所以我总结了一下其原则:从变量名处起,根据运算符优先级结合,一步一步分析.下面让我们先从简单的类型开始慢慢分析吧:

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.4 二级指针

  • 如果一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指向指针的指针变量。也称为“二级指针”
    char c = 'a';
    char *cp;
    cp = &c;
    char **cp2;
    cp2 = &cp;
    printf("c = %c", **cp2);

  • 多级指针的取值规则
int ***m1;  //取值***m1
int *****m2; //取值*****m2

8.5 数组指针的概念及定义

数组元素指针

  • 一个变量有地址,一个数组包含若干元素,每个数组元素也有相应的地址, 指针变量也可以保存数组元素的地址
  • 只要一个指针变量保存了数组元素的地址, 我们就称之为数组元素指针

    printf(“%p %p %p”,p, &(a[0]), a); //输出结果:0x1100, 0x1100, 0x1100
  • 注意: 数组名a不代表整个数组,只代表数组首元素的地址。
  • “p=a;”的作用是“把a数组的首元素的地址赋给指针变量p”,而不是“把数组a各元素的值赋给 p”

8.6 指针访问数组元素

#include <stdio.h>
int main (void)
{
	int a[5] = {2, 4, 6, 8, 22};
    int *p;
    // p = &(a[0]); 
    p = a;
    printf(“%d %d\n”,a[0],*p); // 输出结果: 2, 2
}

#include <stdio.h>
int main (void)
{
	int a[5] = {2, 4, 6, 8, 22};
    int *p;
    // p = &(a[0]); 
    p = a;
    char i=0;
    for(i=0;i<5;i++)
    {
        printf("%d\n",*p);
        p++; 
    }
    return 0;
}
/*
输出结果:
2
4
6
8
22
*/
  • 在指针指向数组元素时,允许以下运算:

    • 加一个整数(用+或+=),如p+1
    • 减一个整数(用-或-=),如p-1
    • 自加运算,如p++,++p
    • 自减运算,如p–,--p
  • 如果指针变量p已指向数组中的一个元素,则p+1指向同一数组中的下一个元素,p-1指向同 一数组中的上一个元素。

  • 结论: 访问数组元素,可用下面两种方法:

    • 下标法, 如a[i]形式
    • 指针法, *(p+i)形式

  • 注意:
    • 数组名虽然是数组的首地址,但是数组名所所保存的数组的首地址是不可以更改的
    int x[10];
	x++;  //错误
	int* p = x;
	p++; //正确

8.7 指针与字符串

定义字符串的两种方式

  • 字符数组
char string[]=”I love rng!”;
printf("%s\n",string);
  • 字符串指针指向字符串
// 数组名保存的是数组第0个元素的地址, 指针也可以保存第0个元素的地址
char *str = "abc"

字符串指针使用注意事项

  • 可以查看字符串的每一个字符
#include <stdio.h>
#include <string.h>
int main()
{
    char *str = "rng";
    int i=0;
    for (i = 0; i < strlen(str); i++)
    {
        printf("%c-", *(str + i)); // 输出结果:r-n-g-
    }
    return 0;
}
  • 不可以修改字符串内容
//   + 使用字符数组来保存的字符串是保存栈里的,保存栈里面东西是可读可写,所以可以修改字符串中的的字符
//   + 使用字符指针来保存字符串,它保存的是字符串常量地址,常量区是只读的,所以我们不可以修改字符串中的字符
char *str = "rng";
*(str+2) = 'y'; // 错误

8.8 指向函数指针

为什么指针可以指向一个函数?

  • 函数作为一段程序,在内存中也要占据部分存储空间,它也有一个起始地址
  • 函数有自己的地址,那就好办了,我们的指针变量就是用来存储地址的。
  • 因此可以利用一个指针指向一个函数。其中,函数名就代表着函数的地址。

指针函数的定义

​ 格式: 返回值类型 (*指针变量名)(形参1, 形参2, ...);

int sum(int a,int b)
{
    return a + b;
}

int (*p)(int,int);
p = sum;

指针函数定义技巧
1、把要指向函数头拷贝过来
2、把函数名称使用小括号括起来
3、在函数名称前面加上一个*
4、修改函数名称

应用场景
调用函数
将函数作为参数在函数间传递

注意点:
由于这类指针变量存储的是一个函数的入口地址,所以对它们作加减运算(比如p++)是无意义的
函数调用中"(指针变量名)"的两边的括号不可少,其中的不应该理解为求值运算,在此处它 只是一种表示符号

8.9 Const型指针

Const是一个C语言的关键字,它限定一个变量不允许被改变,产生静态作用,也就是说经过const 修饰的变量成为只读的变量之后,那么这个变量就只能作为右值(只能赋值给别人),绝对不能成为左值(不能接收别人的赋值)

int const a; 
const int a

上面两条语句都可以将a声明为一个整数,它的值不能被修改。这两种方式你可以任意选一种即可。
常量在定义时可以被初始化。;

Const与指针

常量指针

int const *p;
const int *p;

此时我们无法通过指针改变这个变量的值,但我们可以通过其他的引用来改变变量值

int a=10;
const int* m = &a;
a=11;

常量指针的指向的不能改变,但不意味着指针本身不能改变,常量指针可以指向其他的地址

int b = 10;
int a = 5;
const int* p=&b;//此时无法通过*p去修改b的值
p=&a;//此时无法通过*p去修改a的值
printf("%d",*p);
输出:5

指针常量

指针常量是指指针本身是个常量,不能在指向其他的地址,书写方式如下:

int *const n;

注:指针常量指向的地址不能改变,但是地址中保存的数值是可以改变的,可以通过其他指向改地址的指针来修改。

int a=5;
int *p=&a;
int* const n=&a;
*p=8;

和常量指针相反

  • 指向常量的常指针

此时,指针本身和它指向的数据都是只读的,下面的两种写法能够做到这一点:

const int * const p4;
int const * const p5;

九. 函数

C源程序是由函数组成的

  • 例如: 我们前面学习的课程当中,通过main函数+scanf函数+printf函数+逻辑代码就可以组成一个C语言程序
  • C语言不仅提供了极为丰富的库函数, 还允许用户建立自己定义的函数。用户可把自己的算法编写成一个个相对独立的函数,然后再需要的时候调用它
  • 可以说C程序的全部工作都是由各式各样的函数完成的,所以也把C语言称为函数式语言

9.1 函数的定义

定义函数的目的

  • 将一个常用的功能封装起来,方便以后调用

  • 自定义函数的书写格式

返回值类型 函数名(参数类型 形式参数1,参数类型 形式参数2,…) {
    函数体;
    返回值;
}

void hhh(int x,int y){
    printf("hello world\n");
    retrun 0;
}

定义函数的步骤

  • 函数名:函数叫什么名字
  • 函数体:函数是干啥的,里面包含了什么代码
  • 返回值类型: 函数执行完毕返回什么和调用者

无参无返回值函数定义

  • 没有返回值时return可以省略
  • 格式:
void 函数名() {
    函数体;
}

// 1.没有返回值/没有形参
// 如果一个函数不需要返回任何数据给调用者, 那么返回值类型就是void
void printRose() {
    printf(" {@}\n");
    printf("  |\n");
    printf(" \\|/\n"); // 注意: \是一个特殊的符号(转意字符), 想输出\必须写两个斜线
    printf("  |\n");
  // 如果函数不需要返回数据给调用者, 那么函数中的return可以不写
}

无参有返回值函数定义

  • 格式:
返回值类型 函数名() {
    函数体;
    return 值;
}

int getMax() {
    printf("请输入两个整数, 以逗号隔开, 以回车结束\n");
    int number1, number2;
    scanf("%i,%i", &number1, &number2);
    int max = number1 > number2 ? number1 : number2;
    return max;
}

有参无返回值函数定义

  • 形式参数表列表的格式: 类型 变量名,类型 变量2,......
  • 格式:
void 函数名(参数类型 形式参数1,参数类型 形式参数2,…) {
    函数体;
}

void printMax(int value1, int value2) {
    int max = value1 > value2 ? value1 : value2;
    printf("max = %i\n", max);
}

有参有返回值函数定义

  • 格式:
返回值类型 函数名(参数类型 形式参数1,参数类型 形式参数2,…) {
    函数体;
    return 0;
}

 int printMax(int value1, int value2) {
    int max = value1 > value2 ? value1 : value2;
    return max;
}

函数定义注意

  • 函数名称不能相同
void test() {
}
void test() { // 报错
}

9.2 函数的参数和返回值

形式参数

  • 定义函数时,函数名后面小括号()中定义的变量称为形式参数,简称形参
  • 形参变量只有在被调用时才分配内存单元,在调用结束时,即刻释放所分配的内存单元。
  • 因此,形参只有在函数内部有效,函数调用结束返回主调函数后则不能再使用该形参变量
int max(int number1, int number2, int number3) //  形式参数
{
    return number1 > number2 ? number1 : number2;
}

实际参数

  • 调用函数时, 传入的值称为实际参数,简称实参
  • 实参可以是常量、变量、表达式、函数等,无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值,以便把这些值传送给形参
  • 因此应预先用赋值,输入等办法使实参获得确定值
int main() {
    int num = 99;
    // 88, num, 22+44均能得到一个确定的值, 所以都可以作为实参
    max(88, num, 22+44); // 实际参数
    return 0;
}

指针作为参数

​ 在 C/C++ 语言中实参和形参之间的数据传输是单向的值传递方式,也就是实参可以影响形参,而形参不能影响实参。

指针变量作为参数也不例外,但是可以改变实参指针变量所指向的变量的值

eg

  • 当指针作为形参时,我们仅仅是用于改变指针所指向变量(实参)的值时,这是可以改变的。

int a = 3;
int *p = &a;
void func1(int* val){
	*val = 10;
}
int main(){
	cout<<*p<<endl;	//输出是3;
	func1(p);
	cout<<*p<<endl;	//输出是10;
}
  • 但是要明确的是,当对指针本身进行操作时,其与普通的形参是一样的,没有任何特殊之处。
    就是指针内存单元存放的的其指向地址的值。
int a = 3;
int b = 9;
int *p = &a;
void func1(int* val){
	val = &b;
}
int main(){
	cout<<*p<<endl;	//输出是3;
	func1(p);
	cout<<*p<<endl;	//输出是3;
	/*
		对指针本身所存储的变量的更改,只限于形参形式。
	*/
}

当指针作为形参,而我们要去对其进行实际的操作时。需要采用两种方法:

  • 传入指针的引用
  • 借助高一级指针

int a = 3;
int *p = &a;

void func1(int *&pfather);//指针的引用
void func2(int **pfather);//二级指针

int main(){
	cout<<*p<<endl;	//输出是3;
	func1(p);
	cout<<*p<<endl;	//输出是1;
	func2(&p);
	cout<<*p<<endl;	//输出是9;
	/*
		对指针本身所存储的变量的更改,只限于形参形式。
	*/
}
void func1(int*& pfather){
	int g = 1;
	pfather= &g;
}
void func2(int** pfather){
	int b = 9;
	*pfather= &b;
} 
  • 通过将指针作为函数参数指针的方法,既可以返回多个运算结果,又避 免了使用全局变量。

数组作为函数参数

一维数组作为函数的参数

int strfunc(char *str);    //推荐使用
int strfunc(char str[]);

​ 由于数组名作为参数传递给函数时,函数实际接收到的是一个指针,因此第一种声明是更为准确的。在函数内部sizeof(str)的值将会是字符指针的长度,而不是数组的长度。

​ 编译器同样接受第二种声明形式。数组形参无需写明它的元素数目,是因为函数并不为数组参数分配内存空间,形参只是一个指针。因此数组形参可以与任何长度的数组匹配。如果函数需要知道数组的长度,它必须作为一个显式的参数传递给函数。

多维数组作为函数的参数

​ 一维数组作为函数形参,在声明时可以把它写成数组的形式,也可以把它写成指针形式。但对于多维数组,只有第一维可以进行如此选择,这里的关键在于编译器必须知道第2个及后面各维的长度才能对各下标进行求值,因此在原型中必须声明这些维的长度。你可以使用下面2种形式中的任意一种:

void func(int (*mat)[10]);
void func(int mat[][10]);

但写成下面的原型是不正确的:

void func(int **mat);//其为指向整型指针类型的一个指针,与指向整型数组的指针不一样

形参、实参注意点

  • 调用函数时传递的实参个数必须和函数的形参个数必须保持一致
int max(int number1, int number2) { //  形式参数
    return number1 > number2 ? number1 : number2;
}
int main() {
    // 函数需要2个形参, 但是我们只传递了一个实参, 所以报错
    max(88); // 实际参数
    return 0;
}
  • 形参实参类型不一致, 会自动转换为形参类型
void change(double number1, double number2) {//  形式参数
   // 输出结果: 10.000000, 20.000000
   // 自动将实参转换为double类型后保存
   printf("number1 = %f, number2 = %f", number1, number2);
}
int main() {
    change(10, 20);
    return 0;
}

当使用基本数据类型(char、int、float等)作为实参时,实参和形参之间只是值传递,修改形参的值并不影响到实参函数可以没有形参

void change(int number1, int number2) { //  形式参数
    number1 = 250; // 不会影响实参
    number2 = 222;
}
int main() {
    int a = 88;
    int b = 99;
    change(a, b);
    printf("a  = %d, b = %d", a, b); // 输出结果: 88, 99
    return 0;
}

返回值类型注意点

  • 如果没有写返回值类型,默认是int

  • 函数返回值的类型和return实际返回的值类型应保持一致。如果两者不一致,则以返回值类型为准,自动进行类型转换

int height() {
    return 3.14; 
}
int main() {
  double temp = height();
  printf("%lf", temp);// 输出结果: 3.000000
}
  • 一个函数内部可以多次使用return语句,但是return语句后面的代码就不再被执行
int max(int number1, int number2) {//  形式参数
    return number1 > number2 ? numer1 : number2;
    printf("执行不到"); // 执行不到
    return 250; // 执行不到
}

9.3 函数的声明

在C语言中,函数的定义顺序是有讲究的:

  • 默认情况下,只有后面定义的函数才可以调用前面定义过的函数

  • 如果想把函数的定义写在main函数后面,而且main函数能正常调用这些函数,那就必须在main函数前面进行函数的声明, 否则

    • 系统搞不清楚有没有这个函数
    • 系统搞不清楚这个函数接收几个参数
    • 系统搞不清楚这个函数的返回值类型是什么
  • 所以函数声明,就是在函数调用之前告诉系统, 该函数叫什么名称, 该函数接收几个参数, 该函数的返回值类型是什么

  • 函数的声明格式:

    • 将自定义函数时{}之前的内容拷贝到调用之间即可
      • 例如: int max( int a, int b );
      • 或者: int max( int, int );
// 函数声明
void getMax(int v1, int v2);
int main(int argc, const char * argv[]) {
    getMax(10, 20); // 调用函数
    return 0;
}
// 函数实现
void getMax(int v1, int v2) {
    int max = v1 > v2 ? v1 : v2;
    printf("max = %i\n", max);
}
  • 函数的声明与实现的关系
    • 声明仅仅代表着告诉系统一定有这个函数, 和这个函数的参数、返回值是什么
    • 实现代表着告诉系统, 这个函数具体的业务逻辑是怎么运作的
  • 函数声明注意点:
    • 函数的实现不能重复, 而函数的声明可以重复
// 函数声明
void getMax(int v1, int v2);
void getMax(int v1, int v2);
void getMax(int v1, int v2); // 不会报错
int main(int argc, const char * argv[]) {
    getMax(10, 20); // 调用函数
    return 0;
}
// 函数实现
void getMax(int v1, int v2) {
    int max = v1 > v2 ? v1 : v2;
    printf("max = %i\n", max);
}
  • 函数声明可以写在函数外面,也可以写在函数里面, 只要在调用之前被声明即可
int main(int argc, const char * argv[]) {
    void getMax(int v1, int v2); // 函数声明, 不会报错
    getMax(10, 20); // 调用函数
    return 0;
}
// 函数实现
void getMax(int v1, int v2) {
    int max = v1 > v2 ? v1 : v2;
    printf("max = %i\n", max);
}
  • 当被调函数的函数定义出现在主调函数之前时,在主调函数中也可以不对被调函数再作声明
// 函数实现
void getMax(int v1, int v2) {
    int max = v1 > v2 ? v1 : v2;
    printf("max = %i\n", max);
}
int main(int argc, const char * argv[]) {
    getMax(10, 20); // 调用函数
    return 0;
}
  • 如果被调函数的返回值是整型时,可以不对被调函数作说明,而直接调用
int main(int argc, const char * argv[]) {
    int res = getMin(5, 3); // 不会报错
    printf("result = %d\n", res );
    return 0;
}
int getMin(int num1, int num2) {// 返回int, 不用声明
    return num1 < num2 ? num1 : num2;
}

9.4 全局变量与局部变量

C语言由四种地方可以定义变量

  • 在函数外部定义的是全局变量
  • 在头文件中定义的是全局变量
  • 在函数或语句块内部定义的是局部变量
  • 函数的参数是该函数的局部变量

全局变量:在所有函数外部定义的变量称为全局变量

  • 它的作用域默认是整个程序,也就是所有的源文件,如果给全局变量加上 static 关键字,它的作用域就变成了当前文件,在其它文件中就无效了(内存管理会讲到)。

局部变量:是指在函数内部定义的变量

作用域的区别

作用域是指程序中被定义的变量存在(或生效)的区域,超过该区域变量就不能访问

  • 局部变量的作用域仅限于定义这个变量的函数内部,在一个函数内部定义了,就不能在其它函数内部使用这个变量
  • 全局变量的作用域是整个源程序,也就是整个工程文件,也就是谁说,定义了一个全局变量,这个变量不仅可以在多个函数内部使用,还可以在同一工程中的其它文件中使用
#include<stdio.h>
void swap()
{
	int a = 10;  //在swap函数内定义一个局部变量a
}
int main()
{
	swap();
	printf("%d", a);//在主函数内部是不能使用的
 
}

#include<stdio.h>
int a = 10;  //定义一个全局变量a
void swap()
{
	printf("%d", a);
}
int main()
{
	swap();
	printf("%d", a);//在主函数内部是不能使用的
 
}

生命周期的区别

生命周期是指一个变量的什么时候被创建,什么时候被销毁或被释放。也就是说从创建到释放那一段作用时间,一个变量的生命周期跟它的作用域是紧密相连的。局部变量和全局变量作用域的不同也就导致了它们生命周期的不同,出来这个变量的作用域,这个变量的生命周期也就结束了。

#include<stdio.h>
int j = 0;         //定义一个全局变量j,作用域是整个工程文件
void fun1(int i)   //定义一个局部变量i,作用域在fun1内
{
	i++;
}
void fun2(void)
{
	j++;
}
int main()
{
	int k = 0;
	int a = 0;
	for (k = 0; k < 10; k++)
	{
		fun1(a);
		fun2();
	}
	return 0;
}

当全局变量和局部变量的命名相同时,以局部变量为主

#include<stdio.h>
int a = 10;
int main()
{
    printf("%d",a);  //10
	int a = 30;
	printf("%d",a);  //30
}

9.5 exit与return

  • return是语言级别的,它表示了调用堆栈的返回;
  • 而exit是系统调用级别的,它表示了一个进程的结束。
  • return是返回函数调用,如果返回的是main函数,则为退出程序 。
  • exit是在调用处强行退出程序,运行一次程序就结束。

作为终止程序的方法,return 语句和exit 函数关系紧密,事实上return 表达式 <=> exit(表达式)return 语句和

  • exit 函数之间的差异是:不管哪个函数调用exit 函数都会导致程序终止,return 语句仅当由main函数调用时才会导致程序终止。一些程序员只使用exit 函数,以便更容易定位程序 中的全部退出点。

9.6 main函数的命令行参数

调用主函数时,它带有两个参数。

  • 第一个参数是argc,用于参数计数,它的值表示运行程序时命令行中参数的数目;

  • 第二个参数是argv,用于参数向量,是一个指向字符串数组的指针,其中每个字符串对应一个参数。我们通常用多级指针处理这些字符串。

  • argc是int类型的,它表示的是命令行参数的个数。不需要用户传递,它会根据用户从命令行输入的参数个数,自动确定.

  • argv是char**类型的,有时也写成char* argv[ ] , 它的作用是存储用户从命令行传递进来的参数。它的第一个成员是用户运行的程序名字.

#include <stdio.h>

int main(int argc,char **argv) {
    int i=0;
	printf("argc is %d\nargc is ",argc);
	for(i=0; i<argc; i++) {
		printf("%s ",argv[i]);
	}
	return 0;
}

cyberlove@cyberlove-virtual-machine:~/code/zhizheng$ ./3 I love you
argc is 4
argc is ./3 I love you cyberlove@cyberlove-virtual-machine:~/code/zhizheng$

十. C语言内存管理

10.1 虚拟内存

10.1.1 为什么要有虚拟内存

  • 内存(memory)资源永远都是稀缺的,当越来越多的进程需要越来越来内存时,某些进程会因为得不到内存而无法运行;
  • 内存容易被破坏,一个进程可能误踩其他进程的内存空间;

虚拟内存提供了哪些能力?

  • 高效使用内存:VM将内存看成是存储在磁盘上的地址空间的高速缓存,内存中保存热的数据,根据需要在磁盘和内存之间传送数据;
  • 简化内存管理:VM为每个进程提供了一致的地址空间,从而简化了链接、加载、内存共享等过程;
  • 内存保护:保护每个进程的地址空间不被其他进程破坏。

操作系统会提供一种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来

10.2 内存管理

​ 虚拟地址是通过段表与物理地址进行映射的,分段机制会把程序的虚拟地址分成 4 个段,每个段在段表中有一个项,在这一项找到段的基地址,再加上偏移量,于是就能找到物理内存中的地址。

缺点:会出现内存碎片问题。

10.2.1 内存分页

​ 分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,我们叫页(Page)。在 Linux 下,每一页的大小为 4KB

  • 这么做的好处是页与页之间是紧密排列的,所以不会有内存碎片。
  • 内存分页机制分配内存的最小单位是一页,即使程序不足一页大小,最少也得分配一个页
  • 如果内存空间不够,操作系统会把其他正在运行的进程中的「最近没被使用」的内存页面给释放掉,也就是暂时写在硬盘上,称为换出(Swap Out)。一旦需要的时候,再加载进来,称为换入(Swap In)。
  • 分页的方式使得我们在加载程序的时候,不再需要一次性都把程序加载到物理内存中。我们完全可以在进行虚拟内存和物理内存的页之间的映射之后,并不真的把页加载到物理内存里,而是只有在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去。(这也是8g内存的电脑能玩100多g游戏的原因)

10.2.2 段页式

  • 段页式是内存分段和内存分页的组合方式。段页式内存管理实现的方式:
  • 先将程序划分为多个有逻辑意义的段,也就是前面提到的分段机制;
  • 接着再把每个段划分为多个页,也就是对分段划分出来的连续空间,再划分固定大小的页;

10.3 linux c进程内存

  • 任何一个程序,正常运行都需要内存资源,用来存放诸如变量、常量、函数代码等等。这些不同的内容,所存储的内存区域是不同的,且不同的区域有不同的特性。因此我们需要研究内存布局,逐个了解不同内存区域的特性。
  • 每个C语言进程都拥有一片结构相同的虚拟内存,例如三个完全不相干的进程p1、p2、p3,它们很显然会占据不同区段的物理内存,但经过系统的变换和映射,它们的虚拟内存的布局是完全一样的。
    • PM:Physical Memory,物理内存。
    • VM:Virtual Memory,虚拟内存。

十 一. 结构体

11.1 什么是结构体

  • 结构体和数组一样属于构造类型
  • 数组是用于保存一组相同类型数据的, 而结构体是用于保存一组不同类型数组的
  • 例如,在学生登记表中,姓名应为字符型;学号可为整型或字符型;年龄应为整型;性别应为字符型;成绩可为整型或实型。
  • 显然这组数据不能用数组来存放, 为了解决这个问题,C语言中给出了另一种构造数据类型——“结构(structure)”或叫“结构体”。

定义结构体类型

  • 在使用结构体之前必须先定义结构体类型, 因为C语言不知道你的结构体中需要存储哪些类型数据, 我们必须通过定义结构体类型来告诉C语言, 我们的结构体中需要存储哪些类型的数据
struct 结构体名{
     类型名1 成员名1;
     类型名2 成员名2;
     ……
     类型名n 成员名n;
 };

struct Student {
    char *name; // 姓名
    int age; // 年龄
    float height; // 身高
};

11.2 定义结构体变量

  • 定好好结构体类型之后, 我们就可以利用我们定义的结构体类型来定义结构体变量
  • 格式: struct 结构体名 结构体变量名;
  • 先定义结构体类型,再定义变量
struct Student {
     char *name;
     int age;
 };

 struct Student stu;

  • 定义结构体类型的同时定义变量
struct Student {
    char *name;
    int age;
} stu;
  • 匿名结构体定义结构体变量
struct {
    char *name;
    int age;
} stu;
  • 第三种方法与第二种方法的区别在于,第三种方法中省去了结构体类型名称,而直接给出结构变量,这种结构体最大的问题是结构体类型不能复用

11.3 结构体成员访问

  • 一般对结构体变量的操作是以成员为单位进行的,引用的一般形式为:结构体变量名.成员名
struct Student {
     char *name;
     int age;
 };
 struct Student stu;
 // 访问stu的age成员
 stu.age = 27;
 printf("age = %d", stu.age);

11.4 结构体变量的初始化

  • 定义的同时按顺序初始化
struct Student {
     char *name;
     int age;
 };
struct Student stu = {“lnj", 27};

  • 定义的同时不按顺序初始化
struct Student {
     char *name;
     int age;
 };
struct Student stu = {.age = 35, .name = “lnj"};

  • 先定义后逐个初始化
struct Student {
     char *name;
     int age;
 };
 struct Student stu;
stu.name = "lnj";
stu.age = 35;

  • 先定义后一次性初始化
struct Student {
     char *name;
     int age;
 };
struct Student stu;
stu2 = (struct Student){"lnj", 35};

11.5 结构体类型作用域

  • 结构类型定义在函数内部的作用域与局部变量的作用域是相同的
    • 从定义的那一行开始, 直到遇到return或者大括号结束为止
  • 结构类型定义在函数外部的作用域与全局变量的作用域是相同的
    • 从定义的那一行开始,直到本文件结束为止
//定义一个全局结构体,作用域到文件末尾
struct Person{
    int age;
    char *name;
};

int main(int argc, const char * argv[])
{
    //定义局部结构体名为Person,会屏蔽全局结构体
    //局部结构体作用域,从定义开始到“}”块结束
    struct Person{
        int age;
    };
    // 使用局部结构体类型
    struct Person pp;
    pp.age = 50;
    pp.name = "zbz";

    test();
    return 0;
}

void test() {

    //使用全局的结构体定义结构体变量p
    struct Person p = {10,"sb"};
    printf("%d,%s\n",p.age,p.name);
}

11.6 结构体数组

  • 结构体数组和普通数组并无太大差异, 只不过是数组中的元素都是结构体而已
  • 格式: struct 结构体类型名称 数组名称[元素个数]
struct Student {
    char *name;
    int age;
};
struct Student stu[2]; 

结构体数组初始化和普通数组也一样, 分为先定义后初始化和定义同时初始化

  • 定义同时初始化
struct Student {
    char *name;
    int age;
};
struct Student stu[2] = {{"lnj", 35},{"zs", 18}}; 
  • 先定义后初始化
struct Student {
    char *name;
    int age;
};
struct Student stu[2]; 
stu[0] = {"lnj", 35};
stu[1] = {"zs", 18};

11.7 结构体指针

  • 一个指针变量当用来指向一个结构体变量时,称之为结构体指针变量
  • 格式: struct 结构名 *结构指针变量名
      // 定义一个结构体类型
      struct Student {
          char *name;
          int age;
      };

     // 定义一个结构体变量
     struct Student stu = {"lnj", 18};

     // 定义一个指向结构体的指针变量
     struct Student *p;

    // 指向结构体变量stu
    p = &stu;

     /*
      这时候可以用3种方式访问结构体的成员
      */
     // 方式1:结构体变量名.成员名
     printf("name=%s, age = %d \n", stu.name, stu.age);

     // 方式2:(*指针变量名).成员名
     printf("name=%s, age = %d \n", (*p).name, (*p).age);

     // 方式3:指针变量名->成员名
     printf("name=%s, age = %d \n", p->name, p->age);

     return 0;
 }
  • 通过结构体指针访问结构体成员, 可以通过以下两种方式

    • (*结构指针变量).成员名
    • 结构指针变量->成员名(用熟)
  • (*p)两侧的括号不可少,因为成员符“.”的优先级高于“ * ”。

  • 如去掉括号写作pstu.num则等效于(pstu.num),这样,意义就完全不对了。

11.8 结构体内存分析

  • 给结构体变量开辟存储空间和给普通开辟存储空间一样, 会从内存地址大的位置开始开辟
  • 给结构体成员开辟存储空间和给数组元素开辟存储空间一样, 会从所占用内存地址小的位置开始开辟
  • 结构体变量占用的内存空间永远是所有成员中占用内存最大成员的倍数(对齐问题)
struct Person{
    int age; // 4
    char ch; // 1
    double score; // 8
};
struct Person p;
printf("sizeof = %i\n", sizeof(p)); // 16
  • 占用内存最大属性是score, 占8个字节, 所以第一次会分配8个字节
  • 将第一次分配的8个字节分配给age4个,分配给ch1个, 还剩下3个字节
  • 当需要分配给score时, 发现只剩下3个字节, 所以会再次开辟8个字节存储空间
  • 一共开辟了两次8个字节空间, 所以最终p占用16个字节
    struct Person{
        int age; // 4
        double score; // 8
        char ch; // 1
    };
    struct Person p;
    printf("sizeof = %i\n", sizeof(p)); // 24

  • 占用内存最大属性是score, 占8个字节, 所以第一次会分配8个字节
  • 将第一次分配的8个字节分配给age4个,还剩下4个字节
  • 当需要分配给score时, 发现只剩下4个字节, 所以会再次开辟8个字节存储空间
  • 将新分配的8个字节分配给score, 还剩下0个字节
  • 当需要分配给ch时, 发现上一次分配的已经没有了, 所以会再次开辟8个字节存储空间
  • 一共开辟了3次8个字节空间, 所以最终p占用24个字节

11.9 结构体嵌套定义

  • 成员也可以又是一个结构,即构成了嵌套的结构
struct Date{
     int month;
     int day;
     int year;
}
struct  stu{
    int num;
    char *name;
    char sex;
    struct Date birthday;
    float score;
}

注意:

  • 结构体不可以嵌套自己变量,可以嵌套指向自己这种类型的指针
struct Student {
    int age;
    struct Student stu;
};

对嵌套结构体成员的访问

  • 如果某个成员也是结构体变量,可以连续使用成员运算符"."访问最低一级成员
struct Date {
       int year;
       int month;
       int day;
  };

  struct Student {
      char *name;
      struct Date birthday;
 };

 struct Student stu;
 stu.birthday.year = 1986;
 stu.birthday.month = 9;
 stu.birthday.day = 10;

11.10 结构体和函数

  • 结构体虽然是构造类型, 但是结构体之间赋值是值拷贝, 而不是地址传递
    struct Person{
        char *name;
        int age;
    };
    struct Person p1 = {"lnj", 35};
    struct Person p2;
    p2 = p1;
    p2.name = "zs"; // 修改p2不会影响p1
    printf("p1.name = %s\n", p1.name); // lnj
    printf("p2.name = %s\n", p2.name); //  zs
  • 所以结构体变量作为函数形参时也是值传递, 在函数内修改形参, 不会影响外界实参
#include <stdio.h>

struct Person{
    char *name;
    int age;
};

void test(struct Person per);

int main()
{
    struct Person p1 = {"lnj", 35};
    printf("p1.name = %s\n", p1.name); // lnj
    test(p1);
    printf("p1.name = %s\n", p1.name); // lnj
    return 0;
}
void test(struct Person per){
    per.name = "zs";
    printf("p1.name = %s\n", per.name); // zs
}

11.11 共用体

  • 和结构体不同的是, 结构体的每个成员都是占用一块独立的存储空间, 而共用体所有的成员都占用同一块存储空间
  • 和结构体一样, 共用体在使用之前必须先定义共用体类型, 再定义共用体变量
  • 定义共用体类型格式:
union 共用体名{
    数据类型 属性名称;
    数据类型 属性名称;
    ...   ....
};
  • 特点: 由于所有属性共享同一块内存空间, 所以只要其中一个属性发生了改变, 其它的属性都会受到影响
    union Test{
        int age;
        char ch;
    };
    union Test t;
    printf("sizeof(p) = %i\n", sizeof(t));

    t.age = 33;
    printf("t.age = %i\n", t.age); // 33
    t.ch = 'a';
    printf("t.ch = %c\n", t.ch); // a
    printf("t.age = %i\n", t.age); // 97

11.11.1 共用体的应用场景

(1)通信中的数据包会用到共用体,因为不知道对方会发送什么样的数据包过来,用共用体的话就简单了,定义几种格式的包,收到包之后就可以根据包的格式取出数据。
(2)节约内存。如果有2个很长的数据结构,但不会同时使用,比如一个表示老师,一个表示学生,要统计老师和学生的情况,用结构体就比较浪费内存,这时就可以考虑用共用体来设计。
+(3)某些应用需要大量的临时变量,这些变量类型不同,而且会随时更换。而你的堆栈空间有限,不能同时分配那么多临时变量。这时可以使用共用体让这些变量共享同一个内存空间,这些临时变量不用长期保存,用完即丢,和寄存器差不多,不用维护。

11.12 枚举

什么是枚举类型?

  • 在实际问题中,有些变量的取值被限定在一个有限的范围内。例如,一个星期内只有七天,一年只有十二个月,一个班每周有六门课程等等。如果把这些量说明为整型,字符型或其它类型 显然是不妥当的。
  • C语言提供了一种称为“枚举”的类型。在“枚举”类型的定义中列举出所有可能的取值, 被说明为该“枚举”类型的变量取值不能超过定义的范围。
  • 该说明的是,枚举类型是一种基本数据类型,而不是一种构造类型,因为它不能再分解为任何基本类型。

枚举类型的定义

  • 格式:
enum 枚举名 {
    枚举元素1,
    枚举元素2,
    ……
};

// 表示一年四季
enum Season {
    Spring,
    Summer,
    Autumn,
    Winter
};

枚举变量

  • 先定义枚举类型,再定义枚举变量
enum Season {
    Spring,
    Summer,
    Autumn,
    Winter
};
enum Season s;

  • 定义枚举类型的同时定义枚举变量
enum Season {
    Spring,
    Summer,
    Autumn,
    Winter
} s;
  • 省略枚举名称,直接定义枚举变量(不能复用)
enum {
    Spring,
    Summer,
    Autumn,
    Winter
} s;
  • 枚举类型变量的赋值和使用
enum Season {
    Spring,
    Summer,
    Autumn,
    Winter
} s;
s = Spring; // 等价于 s = 0;
s = 3; // 等价于 s = winter;
printf("%d", s);

枚举使用的注意

  • C语言编译器会将枚举元素(spring、summer等)作为整型常量处理,称为枚举常量。
  • 枚举元素的值取决于定义时各枚举元素排列的先后顺序。默认情况下,第一个枚举元素的值为0,第二个为1,依次顺序加1。
  • 也可以在定义枚举类型时改变枚举元素的值
posted @ 2023-07-20 20:21  hanhuangsi  阅读(408)  评论(1编辑  收藏  举报