1、局部变量和静态变量的区别
-
普通局部变量和静态局部变量区别
-
-
存储位置:
-
-
-
-
普通局部变量存储在栈上
-
静态局部变量存储在静态存储区
-
-
-
-
生命周期:
-
-
-
-
当函数执行完毕时,普通局部变量会被销毁
-
静态局部变量的生命周期则是整个程序运行期间,即使函数调用结束,静态局部变量的值也会被保留
-
-
-
-
初始值:
-
-
-
-
普通局部变量在每次函数调用时都会被初始化,它们的初始值是不确定的,除非显式地进行初始化
-
静态局部变量在第一次函数调用时会被初始化,然后保持其值不变,直到程序结束
-
-
-
#include <stdio.h> void normal_func() { int i = 0; i++; printf("局部变量 i = %d\n", i); } void static_func() { static int j = 0; j++; printf("static局部变量 j = %d\n", j); } int main() { // 调用3次normal_func() normal_func(); normal_func(); normal_func(); // 调用3次static_func() static_func(); static_func(); static_func(); return 0; }
-
运行结果:
-
局部变量 i = 1 局部变量 i = 1 局部变量 i = 1 static局部变量 j = 1 static局部变量 j = 2 static局部变量 j = 3
2、预处理
-
C语言对源程序处理的四个步骤:预处理、编译、汇编、链接。
-
预处理
-
-
宏定义展开、头文件展开、条件编译,这里并不会检查语法
-
-
编译
-
-
检查语法,将预处理后文件编译生成汇编文件
-
-
汇编
-
-
将汇编文件生成目标文件(二进制文件)
-
-
链接
-
-
将目标文件链接为可执行程序
-
gcc -E hello.c -o hello.i //处理文件包含,宏和注释 gcc -S hello.i -o hello.s //编译为汇编文件 gcc -c hello.s -o hello.o //经汇编后为二进制的机器指令 gcc hello.o -o hello //链接所用的到库 1 预处理:预处理相当于根据预处理命令组装成新的 C 程序,不过常以 i 为扩展 名。 2 编 译:将得到的 i 文件翻译成汇编代码 .s 文件。 3 汇 编:将汇编文件翻译成机器指令,并打包成可重定位目标程序的 O 文件。 该文件是二进制文件,字节编码是机器指令。 4 链 接:将引用的其他 O 文件并入到我们程序所在的 o 文件中,处理得到最终 的可执行文件
-
-
C编译器提供的预处理功能主要包括:
-
文件包含 #include
-
宏定义 #define
-
条件编译 #if #endif ……
-
3、文件包含处理
-
文件包含处理
-
指一个源文件可以将另外一个文件的全部内容包含进来
-
C语言提供了#include命令用来实现文件包含的操作
-
-
#include< > 与 #include ""的区别
-
<> 表示系统直接按系统指定的目录检索
-
"" 表示系统先在 "" 指定的路径(没写路径代表当前路径)查找头文件,如果找不到,再按系统指定的目录检索
-
4、宏定义
-
在预编译时将宏名替换成字符串的过程称为"宏展开"(也叫宏替换)。
-
宏名一般用大写,以便于与变量区别
-
宏定义不作语法检查,只有在编译被宏展开后的源程序才会报错
-
宏定不要不要行末加分号
-
#define PI 3.14 #define MAX(a, b) ((a) > (b) ? (a) : (b)) #define FUNC(a) func(a) void func(int a) { int b = a; } int main() { double a = PI; int temp = MAX(1, 2+3); FUNC(10); return 0; }
5、条件编译
一般情况下,源程序中所有的行都参加编译。但有时希望对部分源程序行只在满足一定条件时才编译,即对这部分源程序行指定编译条件。
防止头文件被重复包含
#ifndef _SOMEFILE_H #define _SOMEFILE_H //需要声明的变量、函数 //宏定义 //结构体 #endif
软件裁剪
同样的C源代码,条件选项不同可以编译出不同的可执行程序:
#include <stdio.h> // #define A 有注释,没有注释,观察运行结果 #define A int main() { #ifdef A printf("这是大写操作\n"); #else printf("这是小写操作\n"); #endif return 0; }
6、递归
-
函数递归调用:
-
函数可以调用函数本身(不要用main()调用main(),不是不能这么做,而是不建议,往往得不到你想要的结果)。
-
-
递归的优点
-
递归给某些编程问题提供了最简单的方法。
-
-
递归的缺点
-
一个有缺陷的递归会很快耗尽计算机的资源,递归的程序难以理解和维护
-
7、普通函数调用
#include <stdio.h> void fun_b(int b) { printf("b = %d\n", b); return; } void func_a(int a) { fun_b(a - 1); printf("a = %d\n", a); } int main(void) { func_a(2); printf("main\n"); return 0; }
运行顺序:
-
结论:
-
先调用,后返回(栈结构)
-
调用谁,返回谁的位置
-
运行结果:
b = 1 a = 2 main
8、函数递归调用
#include <stdio.h> //0的阶乘是1 1的阶乘1 return 1 //n! =(n-1)!*n //(n-1)! = (n-2)!*(n-1) //n = 1 // 递归函数计算阶乘 int factorial(int n) { if (n == 0 || n == 1) { return 1; } else { return n * factorial(n - 1); } } int main() { int n; printf("请输入一个整数:"); scanf("%d", &n); // 调用递归函数计算阶乘并输出结果 int result = factorial(n); printf("%d 的阶乘是 %d\n", n, result); return 0; }
运行顺序:
9、大小端验证
所谓的大端模式,是指数据的低位(就是权值较小的后面那几位)保存在内存的高地址中,而数据的高位,保存在内存的低地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放; 所谓的小端模式,是指数据的低位保存在内存的低地址中,而数 据的高位保存在内存的高地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低,和我们的逻辑方法一致。 1)大端模式: 低地址 -----------------> 高地址 0x12 | 0x34 | 0x56 | 0x78 2)小端模式: 低地址 ------------------> 高地址 0x78 | 0x56 | 0x34 | 0x12
#include <stdio.h> #include <stdint.h> int check_endianness() { uint32_t temp = 0x44332211; // 4个字节,32位 uint8_t * p = NULL; // 8位 p = (uint8_t *)&temp; // 只取uint8_t的长度 printf("%#x\n", *p); printf("%#x\n", p[0]); // *p 和 p[0]等价 uint16_t * p1 = (uint16_t *)&temp; printf("*p1 = %#x\n", *p1); if (*p == 0x11 ) { return 0; // 0是小端 } else { return 1; // 大端 } } int main() { int res = check_endianness(); if (res == 0) { printf("小端\n"); } else { printf("大端\n"); } return 0; }
10、大小端转换
#include <stdio.h> int changeBigEndian(int data) { return (data >> 24 & 0x000000ff) | (data >> 8 & 0x0000ff00) | (data << 8 & 0x00ff0000) | (data << 24 & 0xff000000); } int main() { int mem = 0x44332211; printf("%0x\n", changeBigEndian(mem)); return 0; }
11、二分查找
#include <stdio.h> // 二分查找函数 int binarySearch(int arr[], int size, int target) { int left = 0; int right = size - 1; while (left <= right) { int mid = left + (right - left) / 2; if (arr[mid] == target) { return mid; // 找到目标元素,返回索引 } else if (arr[mid] < target) { left = mid + 1; // 在右半部分继续查找 } else { right = mid - 1; // 在左半部分继续查找 } } return -1; // 目标元素不存在,返回-1 } int main() { int arr[] = {1, 3, 5, 7, 9, 11, 13, 15, 17, 19}; int size = sizeof(arr) / sizeof(arr[0]); int target = 11; int index = binarySearch(arr, size, target); if (index != -1) { printf("目标元素 %d 在数组中的索引为 %d\n", target, index); } else { printf("目标元素 %d 不在数组中\n", target); } return 0; }
12、什么是指针,在什么地方使用的
指针(Pointer)是一种特殊的变量类型,它用于存储内存地址。指针的实质就是内存“地址”。 使用范围: 动态内存分配:指针常用于动态分配内存,例如使用 malloc()、calloc() 或 new 分配内存,并使用指针来管理和访问分配的内存块。 数组和字符串:数组名本身就是指向数组第一个元素的指针,在函数参数传递、数组访问等场景中经常用到指针。 函数指针:函数指针是指向函数的指针变量,可以用来在运行时动态确定调用的函数,或者将函数作为参数传递给其他函数。 ……
13、函数指针是什么
函数指针是指向函数的指针变量,它存储了函数的地址,可以用来调用该函数。在 C 语言中,函数名可以视为函数在内存中的地址,因此可以将函数名赋值给函数指针变量,从而实现通过函数指针来调用函数。
#include <stdio.h> int getData(int a, int b) { return a + b; } int main() { int(*func)(int, int); func = getData; printf("%d\n", func(5, 8)); return 0; }
复议:指针函数
-
指针函数是一个返回指针的函数。它的返回值是一个指针,指向某种数据类型的内存地址。
-
指针函数通常用于动态内存分配、返回数组、返回字符串等场景。
int* create_array(int size) { int* arr = malloc(size * sizeof(int)); // 动态分配内存 return arr; }
14、声明和定义的区别
-
声明告诉编译器,某个名称(如变量、函数、类等)存在,但不分配内存空间或提供实现细节。
-
声明通常包括名称和类型信息,以及可能的参数列表。
-
声明可以出现在函数或变量的定义之前,以便在使用之前提供有关名称的信息。
int add(int a, int b);
-
定义不仅声明了名称的存在,还为其分配了内存空间或提供了实现细节。
-
对于变量,定义会分配内存空间;对于函数,定义会提供函数体的实现。
-
每个定义都是一个声明,但不是每个声明都是一个定义。
// 函数定义 int add(int a, int b) { return a + b; }
15、extern关键字是干什么用
用来修饰全局变量,全局变量本身是全局可用的,但是由于文件是单个完成编译,并且编译是自上而下的,所以说,对于不是在本范围内定义的全局变量,要想使用必须用 extern 进行声明,如果不加上 extern ,就会造成重定义。
注意,经 extern 声明的变量,不可以再初始化。
16、位运算
#include <stdio.h> #include <inttypes.h> int main() { // 将变量a的第2位设置为1,其他位保持不变 uint8_t a = 0b10110011; // 0xb3; a |= (1 << 2); // 或者 x = x | (1 << 2); printf("%02x\n", a); // b7, 10110111 // 将变量b的第2位、第6位设置为1,其他位保持不变 uint8_t b = 0b10110011; // 0xb3; b |= (1 << 2 | 1 << 6); printf("%02x\n", b); // f7,11110111 // 将变量c的第5位设置为0,其他位保持不变 uint8_t c = 0b10110011; // 0xb3; c &= ~(1 << 5); printf("%02x\n", c); // 93,10010011 // 将变量d的第0~3位设置为0,其他位保持不变 uint8_t d = 0b11111111; // 0xff; d &= ~(1 << 0 | 1 << 1 | 1 << 2 | 1 << 3); printf("%02x\n", d); // f0,11110000 // 将变量e的第2位取反,其他位保持不变 uint8_t e = 0b10110011; // 0xb3; e ^= (1 << 2); printf("%02x\n", e); // b7, 10110111 return 0; }
17、说说什么是野指针,怎么产生的,如何避免
野指针是指向"垃圾"内存的指针,也就是说,它的值是不确定的。野指针通常由以下几种情况产生: 未初始化的指针:如果你声明了一个指针变量但没有给它赋值,那么它就是一个野指针。例如:int *ptr;。 已删除的指针:如果你使用delete或free删除了一个指针,但没有将它设置为NULL,那么它就成了一个野指针。例如: 超出作用域的指针:如果你返回了一个函数内部的局部变量的地址,那么这个地址在函数返回后就不再有效,因此返回的指针就是一个野指针。 初始化: ptr = NULL;
18、堆和栈有什么区别?
- 栈区(stack) - - 栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。 - 堆区(heap) - - 堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。