C语言学习笔记2

发布时间 2024-01-05 13:27:15作者: Qing-Huan

7. 控制语句

if 语句

不推荐用else,虽然符合业务逻辑,但是不够直观,需要将前面 N 个条件判断看懂,才能明白何种条件下才能进入else语句块中。
因此,下面代码中最好用 else if (data % 3 == 2)

int data = 0;
cout << "请输入一个int数值:";
cin >> data;

if (data % 3 == 0)
{
    cout << data << "对3的余数为0" << endl;
}
else if (data % 3 == 1)
{
    cout << data << "对3的余数为1" << endl;
}
else
{
    cout << data << "对3的余数为2" << endl;
}

switch...case 语句

1. 语法格式

case语句中不能初始化变量

switch (表达式1) // 表达式结果不能是实型、字符串
{
    case 值1:    // 只能是常量表达式:整型(long、int、short、char)
        语句1;
        break;
    case 值2:
        语句2;
        break;
    case 值3:
        语句3;
        break;
    default:
        语句4;
        break;
}

2. C和C++的switch不同点

image

3. switch...case 反汇编

在 case 较少的情况下,switch...case 与if...else 是一样的
image

4. switch...case 跳转表

当 case 较多的情况下,编译器会考虑以空间换时间,对 switch case 进行优化(优化方案:添加跳转表)

由于 case 后的数据为相邻整型变量,所以系统在编译阶段,将这些数值映射为跳转表,将表头地址记录下来用于寻址。

  1. 编译阶段开辟一段连续空间,内部存储每个case语句块的入口地址,将寻址方式写入代码段:jmp dword ptr [edx*4+0B3844h]
  2. 运行时计算 switch值case最小值距离 (求差)
  3. 距离 大于 case最小值case最大值 之差,说明 switch值 大于最大值或小于最小值。直接跳转至default语句块
  4. 距离 传入 edx 中,代入寻址公式跳转
int main()
{
......
	int a = 102;
000B37EA  mov         dword ptr [a],66h  
	switch (a)
000B37F1  mov         eax,dword ptr [a]  
000B37F4  mov         dword ptr [ebp-4Ch],eax  
000B37F7  mov         ecx,dword ptr [ebp-4Ch]  
000B37FA  sub         ecx,64h  
// 和case中的最小值作差,得到从最小case距离与a相等case之间的距离 ---> 2
000B37FD  mov         dword ptr [ebp-4Ch],ecx  
000B3800  cmp         dword ptr [ebp-4Ch],3  
// 比较距离是否在范围内 ---> 2 < 3
000B3804  ja          $LN7+9h (0B3834h)  
// 若距离大于所有case的长度,跳转至default语句块
000B3806  mov         edx,dword ptr [ebp-4Ch]  
000B3809  jmp         dword ptr [edx*4+0B3844h]  
// 以0B3844作为基址,加上距离*4(一个地址4位),此处存储的是语句块的起始地址。
	{
	case 100:
		t = 100;
000B3810  mov         dword ptr [t],64h  
		break;
    ......
	default:
		t = 104;
000B3834  mov         dword ptr [t],68h  
		break;
	}

	return 0;
000B383B  xor         eax,eax  
}

case值间距过大不用跳转表

image

for 循环语句

循环嵌套时:先考虑内层循环,再考虑外层,避免混乱!

for循环语句的汇编实现
image

while 循环语句

image

do...while 循环语句

do
{
    循环体;
}while(表达式1);

image

goto 不要用

8. 数组

一维数值数组

局部变量存储在栈区

  1. 概念:用一段连续的空间,存放相同类型的数据的容器,叫做数组

  2. 定义一个数组,必须知道该数组 有几个 元素,起什么 名字,以及每个元素的 类型

  3. 定义步骤:(注意优先级

    1. 数组名和中括号结合,表明是数组
    2. 数组有几个元素,中括号里就写几
    3. 用数组的元素类型,定义普通变量
    4. 从上往下,整体替换
    定义一个数组有5个元素,每个元素为函数的入口地址,该函数为void
    
    func(int, int)
    arr[5];
    *p;
    void func(int, int);
    
    ----->*arr[5]
    
    ----->void (*arr[5])(int, int);
    
  4. 分析步骤
    [] 结合表示数组,和 * 结合表示指针,和 () 结合表示函数

    int *arr[5]     指针数组
    int (*arr)[5]   数组指针
    int (*arr)(int, int)    函数指针数组
    
  5. 定义数值数组

    int arr[5];
    int len = sizeof(arr) / sizeof(arr[0]);
    for(int i = 0; i < len; i++)
    {
        cout << arr[i] << " ";
    }
    cout << endl;
    
  6. 初始化数值数组

// 全部初始化
// int arr[5] = {10, 20, 30, 40, 50};
// int arr[] = {10, 20, 30, 40, 50};

image

// 部分初始化
// int arr[5] = {10, 20, 30};
// int arr[5] = {0};
int len = sizeof(arr) / sizeof(arr[0]);
for(int i = 0; i < len; i++)
{
    cout << arr[i] << " ";
}
cout << endl;

image

  1. 指定下标初始化
    int arr[] = {1, [3]=5};

  2. 一维数值数组的元素的操作

二维数值数组

局部变量存储在栈区

定义与概念

int arr[3][4];
sizeof(arr); 二维数组总大小
sizeof(arr[0]); 一行数组的大小
sizeof(arr[0][0]); 数组元素的大小

二维数组的行数 row = sizeof(arr) / sizeof(arr[0])
二维数组的列数 col = sizeof(arr[0]) / sizeof(arr[0][0])

初始化

  • 都可以省略行数下标,不能省略列标
  • 未被初始化的部分自动补0
  1. 分段初始化(全部初始化)
    int arr[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};

  2. 分段初始化(部分初始化)
    int arr[3][4] = {{1,2},{5},{9,10,11}};

  3. 连续初始化(全部初始化)
    int arr[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};

  4. 连续初始化(部分初始化)
    int arr[3][4] = {1,2,5,9,10,11};

二维数组在内存中线性存储

image

二维数组排序

#include <stdio.h>

int main(int argc, char* argv[])
{
    int arr[3][4] = {34,15,33,7,2,9,31,67,53,44,89,14};
    int row = sizeof(arr) / sizeof(arr[0]);
    int col = sizeof(arr[0]) / sizeof(arr[0][0]);
    int len = row * col;
    int* p = arr;
    int i = 0;
    for(i = 0; i < len - 1; i++)
    {
        int j = 0;
        for(j = 0; j < len - 1 - i; j++)
        {
            if(*(p+j) > *(p+j+1))
            {
                int tmp = *(p+j);
                *(p+j) = *(p+j+1);
                *(p+j+1) = tmp;
            }
        }
    }

    for(i = 0; i < row; i++)
    {
        int j = 0;
        for(j = 0; j < col; j++)
        {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
    printf("\n");
    return 0;
}

使用方法

变化的值放内层循环。

一维字符数组

局部变量:字符数组在栈区,字符串在只读数据区

初始化

  1. 逐个字符初始化字符数组,未被初始化的部分自动补 '\0'
    char buf[32] = {'h', 'e', 'l', 'l', 'o'};
  • char buf[5] = {'h', 'e', 'l', 'l', 'o'}; 若输出会造成内存泄露,因为字符串读取遇到 '\0' 才会结束,因此让末尾字符为 '\0',或多申请一个字符让其初始化时自动补 '\0'。
  1. 以字符串的形式初始化字符数组
    char buf[16] = "hello";
  • buf[5] = "hello"; 初始化时会内存污染,因为字符串常量会默认在字符串末尾加 '\0'
  1. 字符数组长度可以不写
char buf[] = "hello";
sizeof(buf) = 6

清零

char buf[16] = "";

  • 双引号中看起来没有东西,实际上内部存储了一个 '\0',后面自动补 '\0'

遍历

  1. 用while遍历,以'\0'为判断依据
  2. 若字符串里有'\0',用for循环逐个字符遍历
  3. 增、删、改、查、拷贝、替换、加密、图片信息等面向字符的操作,用for循环逐个字符操作
  4. 直接cout字符串,仅用于查看

字符串

  • 不能获取带空格的字符串
  • C++获取带空格的字符串:cin.getline(buf, sizeof(buf)); sizeof(buf)中默认包含一个 '\0',所以传4的时候,输出的字符串有3个字符+1个 '\0'
  • 字符串是一个变量,使用时给出一个地址,就算是局部变量 char str = "a" 也不会存在栈区。

字符串输入

  • scanf和%s结合不能获取带空格的字符串,如果首次遇到空格或回车会自动跳过
  • gets能获取带空格的字符串,遇到换行符结束(不包含换行符),但是获取输入内容的时候,不会判断字符串大小,容易造成访问越界。
  • fget解决了gets的不安全问题,(并且包含换行符)
    char *fgets(char *s, int size, FILE *stream);
    fgets(buf, sizeof(buf), stdin);
    buf[strlen(buf) - 1] = '\0'; // 清掉回车
    

二维字符数组

  • 二维字符数组局部变量存在栈区,字符串常量存在只读数据区。初始化时以4位为一个块进行拷贝,剩余部分以4位 '\0' 补位

image

  • 字符串常量在只读数据区的话,是连续存储的么?通过反汇编观察到,不一定连续

image

  • 二维数组中的某一行越界,造成什么结果?
    发现汇编指令用的是 rep movs ,rep是重复指令,依赖ecx(计数寄存器)重复了多少次。上面存在 mov ecx, 8 ,那么8是从何而来呢?
    首先这条指令 rep movs dword ptr es:[edi],dword ptr [esi] 用的是 dword 4字节拷贝,而二维字符数组的列是32位,32 除 4 等于8 ,刚好将字符数组的一行装满。

image

以下初始化的区别?

char buf[4][32] = {'\0'}; // 存在栈区吧?
char buf[4][32] = {""}; // 存在只读数据区?

image

char buf[32] = "";
char buf[32] = {""};

image

image

strlen和sizeof的区别

  • strlen函数测量字符串变量的长度(遇到 '\0' 结束测量,长度不包含 '\0' )
  • sizeof函数测量字符串类型的长度,包含 '\0'
    • 【注】从数组作为函数的形参中能明确该问题,不能在函数里用sizeof计算大小,因为它是指针变量或数组指针变量,但是能用strlen计算字符串大小,因为它遇到'\0'结束
  • sizeof是关键字,编译器就计算出结果,将结果直接作为常量写在代码段

image

9. 函数

函数定义原则

  • 函数的功能要单一
  • 除业务必须使用外,不要用全局变量,会破坏函数封装性
  • 封装性。不管调用几次,函数只有一份,节约空间
  • 函数调用需要出入栈的开销,耗费时间。典型的 用时间换空间

函数的定义、声明、调用

  1. 定义:确定函数名、返回值类型、形参、实现 函数体
  2. 调用:函数名(实参);本质是执行函数体的功能代码
  3. 声明:对函数名、返回值类型、形参类型进行提前声明,不会实现函数体

形参、实参、返回值

  • 形参:定义函数的时候 () 里面的参数为函数形参

    • 在定义函数的时候,形参不占空间。只有 调用函数的时候,系统才给形参 开辟空间【入栈】
      • 假设若定义时就开辟空间,但是没有调用,浪费空间
      • 若不管浪费空间的问题,定义时开辟,若同时多次调用会冲突
    • 有些编译器会将 参数存入寄存器 ,不同编译器可以接参的寄存器和个数不一样,需要测试一下。寄存器个数不够,会继续入栈
    • 本质就是局部变量,函数结束,形参会被释放【调用约定、平栈】
  • 实参:调用函数时,传递给函数的参数值,就是实参

    • 实参传递给形参的方式只有单向传递,只是单向传递又分为:传值、传地址、传引用。
  • 函数返回值:
    若函数无返回值,默认为int
    image

返回值小于4B,存入寄存器EAX中

image

返回值大于4B小于8B,存入寄存器EAX和EDX中

image

返回值大于8B放入栈区的临时空间
将所有参数入栈后,最后将一个临时空间地址入栈,当被调函数返回前,会将返回值存入临时空间,将临时空间的地址存入寄存器EAX中返回给主调函数

image

函数调用流程

Linux 是由shell解析器bash进程调起的main函数

形参类型

  • 普通变量作为函数的形参,函数内部只能对外部变量进行读操作,不能修改外部变量值

  • 数组作为函数的形参,那么函数内部可以操作外部数组的元素。eg:形参arr完全等价于外部的实参arr

  • 二维数组做函数形参时,列下标不能省略。并且函数里面的二维数组求sizeof和函数外不一样。

10. 排序

冒泡排序

每次两两比较,将最大值交换到最后。

外层循环可以看成:需要找到 n-1 个最大值就可以排序成功,最后一个数不需要比较即为最小值。循环条件为:【i<n-1

内层循环中,两两相比【j 与 j+1】,为了保证不越界,j 要小于n,并且j+1 也要小于n,取教小范围【j+1<n ---> j<n-1】。并且每次外层循环都能找到一个最大值,已找到的最大值不再需要比较,因此循环条件为:【j < n - 1 - i

优化:内层循环若一次都没有交换,那么前面数据就是有序的,可以跳出排序了。

for(i = 0; i < n - 1; i++)
{
    for(j = 0; j < n - i - 1; j++)
    {
        if(arr[j] > arr[j+1])
        {
            arr[j] = arr[j] ^ arr[j+1];
            arr[j+1] = arr[j] ^ arr[j+1];
            arr[j] = arr[j] ^ arr[j+1];
            flag = 1;
        }
    }
    if(0 == flag)
    {
        break;
    }
}

选择排序

第i位 为假设最小值,遍历时比较并记录最小值的下标,若下标与 第i位 不同,则交换。

内层循环每次的记录值都是 i,并且每次都从 i的下一位 开始比较。

for(i = 0; i < n - 1; i++)
{
    int tmp = 0, j = 0;
    for(tmp = i, j = i + 1; j < n; j++)
    {
        if(arr[tmp] > arr[j])
            tmp = j;
    }
    if(i != tmp)
    {
        arr[i] = arr[i] ^ arr[tmp];
        arr[tmp] = arr[i] ^ arr[tmp];
        arr[i] = arr[i] ^ arr[tmp];
    }
}

11. 指针

大小端

  • 低低高高为小端
    低地址存低位,高地址存高位

物理地址通过MMU转化为虚拟地址

  • 32位平台,地址编号的大小是4字节

  • 64位平台,地址编号的大小是8字节

  • 类型可以认为是为了区分内存大小

  • 指针本质指的是地址编号的类型

  • 指针类型变量都是4字节/8字节(32位/64位)

地址和指针的区别

地址:系统为内存的每一个字节分配的编号
指针:指的是地址编号的类型
指针变量:本质是变量,存储的是地址编号
指针根据类型的大小,可以从存储的地址编号开始访问

定义指针变量的步骤

  1. *修饰指针变量名
  2. 保存谁的地址就定义谁
  3. 从上往下整体替换

案例1:定义一个指针变量p,保存int data 的地址
int data = 100;

  1. *p
  2. int data
  3. int (*p)
  4. int *p
  5. p = &data

案例2:定义一个指针变量p,保存int arr[5] 的首地址

  1. *p
  2. int arr[5];
  3. int (*p)[5]; // 数组指针

案例3:定义一个指针变量p,保存int my_add(int, int)入口地址

  1. *p
  2. int my_add(int, int)
  3. int (*p)(int, int) //函数指针

指针变量的定义分析

在定义的时候,*的作用 是描述p为指针变量,变量名为p
int *p;

int data;
// 定义的时候,*描述p为指针变量,&data赋值给p,而不是*p
int *p = &data;

指针变量的初始化

  1. 初始化为合法的空间(较少使用)
    int data;
    int *p = &data;

  2. 初始化为空
    int *p = NULL;// 给p赋值,不是*p哦

    • #define NULL (void*)0 是0地址编号
    • 只有当p指向合法地址空间时,才能使用

通过指针变量访问空间内容

*p == data == 100;

*p 取地址
mov eax, dword ptr[p]
mov ecx, dword ptr[eax]

在使用中:*p表示取p所保存地址编号对应空间的内容

指针变量相关类型

int data = 0;
int *p = &data;

  1. 指针变量自身的类型
    • 将变量名去掉,剩下啥类型,自身就是啥类型。p自身类型是int*
    • 仅为赋值时使用,判断类型是否匹配
  2. 指针变量指向的类型
    • 将变量名以及离变量名最近的一个* 一起去掉,剩下什么就是指向什么类型。p指向的类型为int
    • p保存了 data的地址 == p指向了 data == p指向类型就决定了p能保存指向类型定义的变量的地址
      eg:p指向类型为int 就决定了 p能保存int定义的变量的地址。

指针变量的指向类型的作用

  1. 指向类型的大小 决定了 指针变量的取值宽度
  2. 指向类型的大小 决定了 指针变量+1的跨度

&和*的关系

int data = 0; // data的类型为int
&data;// 整个表达式的类型为int*
int *p; // p 的类型为int*
&p; // 整个表达式的类型为int**
对变量取地址的类型为变量的类型+*

int data = 0;
int *p = &data
p的类型为int*
*p整体的类型为int
对地址取内容的类型为地址类型基础上-*

在使用中,&和*一起出现,从右往左依次抵消。

void

  • sizeof(void) == 1是编译器特点,C语言中规定是没有大小的
  • void不能定义普通变量
    系统无法根据data的void类型为其开辟空间,所以定义失败
  • void*可以定义变量,是万能指针
  • void*是指针类型,本质是指针类型,32位平台任何指针都是4字节,64位平台是8字节
    可以保存任何一级指针类型的地址
  • +1会报错,不能跳,*p会报错,因为指向类型是void,void类型没有宽度和跨度
  • 强制类型转换,才能+1或取*,但不改变p自身类型(void*)
  • 多数作为函数参数使用,达到函数功能通用的目的

数组指针

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

  • arr数组名 代表的是数组首元素的地址,不是数组首地址
  • arr + 1代表第1个元素地址,
  • 数组名是符号常量,不能被赋值
    arr++会报错
  • 数组元素的指针变量,
int arr[5] = {100,200,300,400,500};
int *p = arr;
cout << *p++ << endl;   // 100
// * 与 ++ 的运算符优先级相同,从右向左结合
//
cout << *(p++) << endl; // 200
cout << (*p)++ << endl; // 300

[] * ()区别

arr[1] ==> *(arr + 1) == *(1 + arr) == 1[arr]
[]是*()的缩写:[]左边的数写在+左边,[]里面的数写在+右边
&arr[0] == &*(arr + 0) == arr + 0 == arr
数组的中括号里可以为负数

int arr[5] = {100, 200, 300, 400, 500};
int* p = arr
cout << p[3] << endl;
// *(p+3) = 400

指针运算

指针减法含义:两个地址之间的元素个数
指向同一个数组的两个指针相减,不是单纯的地址相减,相减后除以一个单位的大小
指向同一个数组的两个不同类型指针不能相减,会报错,强转后可以
指向同一个数组的两个指针相加无意义

二维数组

二维数组的数组名代表首行的首地址,+1 跳过一行
数组指针代表数组的首地址,+1 跳过一个数组