c语言知识整理
一 数据的存储
对于整形的存储 无论是正负在存储中都是使用补码进行存储的 那个一个数字的补码在转换正负时不同的 对于存储中 首位一定是符号位 如果是0 那么是正数 如果是1 那么是负数 (32位 除符号位 缺少的位数使用0补齐)
如果是正数 那么其 原码 反码 补码都是相同的 原码就是这个数的二进制
如果是负数 那么原码是其二进制位时的表示 反码就是将原码除符号位之外进行按位去反
补码就是再对求出的反码进行+1 也就是原码-> 转换为补码 就是取反之后+1 反之 补码变原码也是取反 +1
大小端存储 在不同的编译器环境下 使用的存储可能是大端也可能是小端
如果数据的小端 放在内存地址中的低地址 数据的大端 放在内存中高地址处 那么这样就是小端的存储方式 反之
数据的小端 放在内存地址中的高地址 数据的大端 放在内存中低地址处 那么这样就是小端的存储方式
写一段代码 判断这个环境下存储使用的是大端还是小端
#include<stdio.h>int func()
{int f = 1;return (*(char*) & f);
}
int main()
{if (func() == 1){printf("小端存储\n");}else{printf("大端存储\n");}return 0;
}
此外还可以通过联合体公用同一块地址的方式 在判断使用的是大端还是小端存储
#include<stdio.h>
int func()
{union{int i;char r;}s;s.i = 1;return s.r;
}
整形类型之间的互相赋值时会发生截断和提升问题
浮点数的存储问题
任意⼀个⼆进制浮点数V可以表⽰成下⾯的形式
V = (−1) ∗ S M ∗ 2 E
• (−1) 表⽰符号位,当S=0,V为正数;当S=1,V为负数 S
• M表⽰有效数字,M是⼤于等于1,⼩于2的
• 2 表⽰指数位
32 位浮点数(单精度)
-
符号位(S):1 位,最高位,0 表示正数,1 表示负数。
-
指数位(E):8 位。
-
尾数位(M):23 位。
64 位浮点数(双精度)
-
符号位(S):1 位,最高位,0 表示正数,1 表示负数。
-
指数位(E):11 位。
-
尾数位(M):52 位。
为什么浮点数和整数的解读结果差别很大?
-
存储格式不同:
-
浮点数:存储的是符号位、指数位和尾数位。
-
整数:存储的是直接的二进制数值。
-
-
解读方式不同:
-
浮点数:需要按照 IEEE 754 标准的公式 V=(−1)S×M×2E 来解读。
-
整数:直接将二进制序列转换为十进制数值。
-
c语言中常见的库函数
首先接受的字符串协函数
1.size_t strlen ( const char * str );
这里的字符串中必须存在\0
strlen函数会在找到\0 之后停止 并且返回 \0 之前的字符串长度
这里需要包含头文件string.h
模拟strlen函数
// 不使用临时变量模拟strlen
size_t mystrlen(const char* arr)
{assert(arr);if (*arr == '\0'){return 0;}else{return mystrlen(arr+1)+1;}}
int main()
{char s[] = "1231456";printf("%d\n", mystrlen(s));s[2] = '\0';printf("%d\n", mystrlen(s));return 0;
}
#include<stdio.h>
#include<string.h>
#include<assert.h>
size_t mystrlen(const char * arr)
{assert(arr);int ret = 0; while (*arr){ret++;arr++;}return ret;
}
int main()
{char s[] = "1231456";printf("%d\n", mystrlen(s));s[2] = '\0';printf("%d\n", mystrlen(s));return 0;
}
2.char* strcpy(char * destination, const char * source );
strcpy会将 source中的内容复制destination
首先source 中必须存在'\0' 目标数组中必须是可以修改的
destination中必须有足够多的数组大小来存储source中的内容 并且最后会将源字符串中的\0也会拷贝到目的数组中
模拟strcpy函数
char* mystrcpy( char* din , const char* sour )
{assert(din);assert(sour);char* s1 = din;while (*din++ = *sour++){;}return s1;
}int main()
{char c1[] = " 123545";const char* c2 = "wqeqwr";mystrcpy(c1 , c2);printf("%s\n", c1);return 0;
}
3.char *my_strcat(char *dest, const char*src);
使用这个函数 无论是dest 韩式 src字符串都要存在\0 且dest字符串时可以修改的 dest字符串的容量足以存下dest+src的容量
strcat 会将src中的内容追加到dest字符串中 从覆盖dest\0的位置开始追加
模拟strcat 函数
char* mystrcat(char* dest , const char* src)
{assert(dest);assert(src);char* ret = dest;while (*dest){dest++;}while (*dest++ = *src++){;}return ret;
}
int main()
{char c1[20] = "123545";const char* c2 = "wqeqwr";mystrcat(c1 , c2);printf("%s\n", c1);return 0;
}
4.int strcmp ( const char * str1, const char * str2 );
这里是要比较两个字符串的大小 首先比较是按照字典序进行比较的 如果两者在第一位的字符相同 则继续比较第二位的字符 如果两个字符串 在前面的比较都相同 则字符串长度较长的大 如果两个字符串长度相同 且每一位的字符相同的话 则两个字符串时相同的
模拟 strcmp
int mystrcmp(const char * c1 , const char * c2)
{assert(c1);assert(c2);while (*c1 && * c2){if (*c1 > *c2)return 1;else if (*c1 < *c2)return -1;c1++;c2++;}if (*c1 == '\0' && *c2 == '\0')return 0;else if (*c1 == '\0')return -1;else if (*c2 == '\0')return 1;
}
int main()
{char c1[20] = "55511";char c2[] = "555111";int ret = mystrcmp(c1 , c2);printf("%d\n", ret);return 0;
}
char * strstr ( const char * str1, const char * str2);
strstr是在str1中 找str2完全匹配的子字符串 并且将这个在str1中第一次找到位置进行返回 找寻的范围知道str1中第一次出现\0结束
模拟strstr
char* mystrstr(const char* c1 , const char*c2)
{char* cp = (char*)c1;if (!*c2) return (char*)c1;while (*cp){char* s1 = (char *)cp;char* s2 = (char*) c2;while (*s1 && *s2 && *s1 == *s2){s1++;s2++;}if (*s2 == '\0')return (char * )cp;cp++;}return (NULL);
}int main()
{char str[] = "This is a simple string";char* pch;pch = mystrstr(str, "simple");printf("%d\n", pch - str);return 0;
}
内存函数
1.void * memcpy ( void * destination, const void * source, size_t num );
memcpy
的行为
按字节复制:
memcpy
会从 source
指定的内存位置开始,逐字节复制数据到 destination
指定的内存位置。
它会复制 num
个字节的数据。
不检查 '\0':
memcpy
不会检查数据中的内容,它不会在遇到 '\0'(空字符)时停止复制。这与 strcpy
函数不同,strcpy
会在遇到 '\0' 时停止复制。
内存重叠问题:
如果 source
和 destination
有重叠部分,memcpy
的行为是未定义的。这意味着结果可能是不可预测的,具体行为取决于编译器和硬件实现
注意事项
确保目标内存足够大:
在调用 memcpy
之前,确保目标内存有足够的空间来存储复制的数据,避免内存溢出。
避免内存重叠:
如果可能涉及内存重叠,使用 memmove
而不是 memcpy
。
检查指针有效性:
确保 source
和 destination
指针有效,避免野指针或空指针导致的运行时错误。
模拟实现memcpy
#include<stdio.h>
void* mymemcpy(void*dest ,const void* src , size_t num)
{void* ret = dest;assert(dest);assert(src);while (num--){*(char*)dest = *(char*)src;dest = (char*)dest + 1;src = (char*)src + 1;}return ret;
}
int main()
{int arr1[] = {1,2,3,4,5,6,7,8,9,10};int arr2[10] = {0};mymemcpy(arr2,arr1 , sizeof(int) * 7);for (int i = 0; i < 10; i++)printf("%d ", arr2[i]);return 0;
}
2. void * memmove ( void * destination, const void * source, size_t num );
这里的使用方式和上面的memcpy基本一致 但是memmove能够解决上面memcpy中的复制自己时产生的重叠问题
模拟memmove
void* mymemmove(void* dest, const void* src, size_t num)
{void* ret = dest;assert(dest);assert(src);if (dest <= src ||(char*)dest >= (char*)src+ num ){while (num--){*(char*)dest = *(char*)src;dest = (char*)dest + 1;src = (char*)src + 1;}}else{dest = (char*)dest + num - 1;src = (char*)src + num - 1;while (num--){*(char*)dest = *(char*)src;dest = (char*)dest -1;src = (char*)src -1;}}return ret;
}
int main()
{int arr1[] = {1,2,3,4,5,6,7,8,9,10};int arr2[10] = {0};mymemmove(arr1+2,arr1 , sizeof(int) * 5);for (int i = 0; i < 10; i++)printf("%d ", arr1[i]);return 0;
}
3.void * memset ( void * ptr, int value, size_t num );
#include <stdio.h>
#include <string.h>
int main ()
{char str[] = "hello world";memset (str,'x',6);printf(str);return 0;
}
输出的结果:xxxxxxworld
自定义类型
结构体 struct
结构体是数据的集合
结构体内存对齐规则
-
第一个成员的对齐方式:
-
结构体的第一个成员总是对齐到与结构体变量起始位置偏移量为0的地址处。也就是说,第一个成员的地址就是结构体的起始地址。
-
-
其他成员的对齐方式:
-
其他成员变量需要对齐到某个数字(对齐数)的整数倍的地址处。
-
对齐数 = 编译器默认的对齐数与该成员变量大小的较小值。
-
在Visual Studio(VS)中,默认的对齐数为8。
-
在Linux中,使用
gcc
编译器时,没有默认对齐数,对齐数就是成员自身的大小。
-
-
-
结构体总大小的计算:
-
结构体的总大小必须是结构体中最大对齐数的整数倍。
-
如果结构体的最后一个成员的结束位置没有达到最大对齐数的整数倍,编译器会在结构体的末尾添加填充字节,以确保结构体的总大小符合对齐要求。
-
-
嵌套结构体的对齐方式:
-
如果结构体中嵌套了其他结构体,嵌套的结构体成员需要对齐到其成员中最大对齐数的整数倍处。
-
结构体的整体大小是所有最大对齐数(包括嵌套结构体中成员的对齐数)的整数倍。
-
为什么要有内存对齐规则 ?
1.不同的硬件平台对内存访问有不同的要求和限制。这些限制主要源于硬件设计和指令集架构。
2.内存对齐可以显著提高内存访问的效率,从而提升程序的整体性能。如果数据未对齐,处理器可能需要进行两次内存访问,因为数据可能跨越两个内存块。
修改默认对⻬数 #pragma 这个预处理指令,可以改变编译器的默认对⻬数。
#pragma pack(1)//设置默认对⻬数为1
struct S
{char c1;int i;char c2;
};
#pragma pack()//取消设置的对⻬数,还原为默认
int main()
{//输出的结果是什么? printf("%d\n", sizeof(struct S));return 0;
}
结构体位段
位段的定义
位段是一种结构体或联合体中的成员,它允许指定每个成员占用的位数。位段的声明方式类似于普通结构体成员,但有两个主要区别:
-
成员类型限制:
-
位段的成员类型通常是
int
、unsigned int
或signed int
。 -
在C99标准中,位段成员的类型可以扩展到其他整数类型(如
char
、short
等),但具体支持情况取决于编译器。
-
-
位宽指定:
-
位段成员名后面必须有一个冒号和一个数字,表示该成员占用的位数。
-
struct BitField {unsigned int field1 : 3; // 占用3位unsigned int field2 : 5; // 占用5位int field3 : 4; // 占用4位
};
总结 :跟结构相⽐,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
联合体
union Un
{char c;int i;
};
联合体的特点 联合的成员是共⽤同⼀块内存空间的,这样⼀个联合变量的⼤⼩,⾄少是最⼤成员的⼤⼩(因为联合 ⾄少得有能⼒保存最⼤的那个成员)。
联合体⼤⼩的计算
联合的⼤⼩⾄少是最⼤成员的⼤⼩。
当最⼤成员⼤⼩不是最⼤对⻬数的整数倍的时候,就要对⻬到最⼤对⻬数的整数倍。
枚举
enum Day//星期
{Mon,Tues,Wed,Thur,Fri,Sat,Sun
};
{}中的内容是枚举类型的可能取值,也叫 枚举常量 。 这些可能取值都是有值的,默认从0开始,依次递增1,当然在声明枚举类型的时候也可以赋初值。
优点总结
-
可读性和可维护性:枚举类型通过赋予常量有意义的名称,使代码更易于理解和维护。
-
类型检查:枚举类型在编译时进行类型检查,避免错误的常量值被使用。
-
调试方便:枚举类型的常量名称在调试时会保留,便于调试。
-
使用方便:枚举类型可以一次性定义多个常量,自动赋值,减少代码量。
-
作用域规则:枚举类型可以定义在函数内部,避免全局命名冲突,使代码更加模块化。
为什么使用枚举而不是#define
?
虽然#define
可以定义常量,但它缺乏类型检查和作用域控制,容易导致错误和命名冲突。枚举类型提供了更强大的功能和更高的安全性,是定义一组相关常量的更好选择。
enum Color//颜⾊
{RED=1,GREEN=2,BLUE=4
};
enum Color clr = GREEN;//使⽤枚举常量给枚举变量赋值
内存管理
为什么要有动态内存分配?
灵活的内存管理
-
运行时确定大小:在许多情况下,程序在编译时并不知道需要多少内存。例如,用户输入的数据量、文件大小或网络传输的数据量等,这些信息只有在程序运行时才能确定。动态内存分配允许程序在运行时根据实际需要申请内存。
-
调整内存大小:使用动态内存分配,程序可以根据需要增加或减少内存的使用。这对于处理可变大小的数据集非常有用。
优化内存使用
-
按需分配:动态内存分配允许程序仅在需要时才分配内存,这样可以避免不必要的内存浪费。
-
内存重用:通过动态内存分配,程序可以释放不再使用的内存,这些内存可以被操作系统回收并重新分配给其他需要的程序,从而提高内存的利用率。
malloc realloc calloc 区别
malloc
和 calloc
的区别
-
初始化:
-
malloc
:申请的内存空间不会进行初始化,其内容是不确定的,可能是之前使用过的内存中残留的数据。 -
calloc
:会将申请的内存空间全部初始化为0。
-
-
参数:
-
malloc
:只需要传入一个参数,即申请的总字节数。 -
calloc
:需要传入两个参数,第一个是元素个数,第二个是每个元素的大小。calloc
会计算这两个参数的乘积,得到总字节数,并分配相应大小的内存。
-
realloc
和它们两个的区别
-
功能:
-
realloc
:用于对已经申请好的内存空间进行大小调整。它可以增加或减少已分配内存的大小,并返回一个新的指针。如果调整后的内存大小比原来的小,可能会丢失超出新大小的数据;如果比原来的大,新增加的部分内容是不确定的。 -
malloc
和calloc
:用于申请新的内存空间,不涉及对已分配内存的调整。
-
-
参数:
-
realloc
:需要传入两个参数,第一个是已分配内存的指针,第二个是期望的总字节数。 -
malloc
和calloc
:只需要传入一个参数(malloc
)或两个参数(calloc
),用于指定申请的内存大小。
-
C/C++中程序内存区域划分
栈区(stack):在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时 这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中,效率很⾼,但是分配的内 存容量有限。栈区主要存放运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等。
-
特点:
-
速度快:由于栈内存的分配和释放非常快速,因此适用于存储生命周期较短的数据。
-
容量有限:与堆相比,栈的容量通常较小,过多的递归调用或大数组可能会导致栈溢出。
-
后进先出(LIFO):栈是一种后进先出的数据结构,最后压入的数据会最先被弹出。
-
2. 堆区(heap):⼀般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配⽅ 式类似于链表。动态分配的内存,如使用malloc
、calloc
、realloc
和free
等函数分配的内存。
-
特点:
-
灵活性高:堆内存的大小不固定,可以根据需要动态调整,适合存储生命周期较长的数据。
-
分配速度较慢:相比栈内存,堆内存的分配和释放速度较慢,因为需要进行内存管理操作。
-
碎片化问题:频繁的分配和释放可能导致堆内存碎片化,影响内存使用效率。
-
3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
-
特点:
-
生命周期长:静态内存的生命周期贯穿整个程序的运行过程,直到程序结束。
-
初始化:未显式初始化的全局变量和静态变量会被自动初始化为0。
-
4. 代码段:存放函数体(类成员函数和全局函数)的⼆进制代码。
-
特点:
-
只读:代码段通常是只读的,以防止程序运行时被意外修改。
-
共享:在某些系统中,多个程序可能会共享相同的代码段,以节省内存空间。
-
文件操作
程序文件
程序文件通常指的是包含程序代码的文件,它们可以是源代码文件、目标文件或可执行文件。这些文件在程序的编译和执行过程中起着重要作用。源程序文件(.c)目标文件(.obj)可执行程序(.exe)
数据文件
数据文件是程序运行时读写的数据文件,它们不包含程序代码,而是包含程序需要处理的数据。
数据文件 又可以分为二进制文件 和 文本文件
文本文件
以ASCII码的形式存储数据,每个数字由字符表示。文本文件可读性强,但可能包含额外的字符编码信息。
-
用途:
-
文本文件常用于存储配置信息、源代码、日志文件等。
-
它们也常用于数据交换,如CSV文件可以被Excel等电子表格软件读取。
-
二进制文件
直接存储数据的二进制表示,不包含可读的字符。二进制文件不可读,但存储和传输效率更高。
-
用途:
-
二进制文件常用于存储图像、音频、视频等多媒体数据。
-
它们也常用于存储复杂的数据结构和对象,如数据库文件、可执行文件等。
-
⽂件的随机读写
seek 根据⽂件指针的位置和偏移量来定位⽂件指针
ftell 返回⽂件指针相对于起始位置的偏移量
rewind 让⽂件指针的位置回到⽂件的起始位置
对⽐⼀组函数:scanf/fscanf/sscanf
printf/fprintf/sprintf
scanf
、fscanf
和 sscanf
用于从不同来源读取格式化输入,其中 scanf
从标准输入读取,fscanf
从文件读取,sscanf
从字符串读取。
printf
、fprintf
和 sprintf
用于输出格式化字符串,其中 printf
输出到标准输出,fprintf
输出到文件,sprintf
输出到字符串。
这些函数在参数和返回值上有所不同,但都遵循类似的格式化规则,使得它们可以灵活地处理各种输入输出任务。
头文件中的 #ifndef
/#define
/#endif
是干什么用的?
这三个预处理指令通常一起使用,用于防止头文件内容被重复包含,称为“包含保护”。这种结构确保了即使头文件被多次包含,其内容也只会被编译一次,避免了重复定义等问题。
-
#ifndef
:表示“如果未定义”。预处理器会检查指定的宏是否已被定义,如果没有定义,则执行接下来的代码。 -
#define
:用于定义一个宏。在包含保护中,通常定义一个与头文件名相关的宏。 -
#endif
:表示“结束条件编译”。它标志着#ifndef
块的结束。
#include <filename.h>
和 #include "filename.h"
有什么区别?
这两种指令都是用于包含头文件,但它们在搜索头文件时的路径不同:
-
#include <filename.h>
:指示编译器在系统标准库路径中搜索头文件。这些路径通常是编译器安装时配置的,用户一般不需要修改。 -
#include "filename.h"
:指示编译器首先在当前源文件所在的目录中搜索头文件,如果没有找到,再在系统标准库路径中搜索。这种搜索方式更灵活,通常用于包含用户自定义的头文件。
C/C++程序编译链接过程?分别完成了什么事情?
-
预处理:
-
处理预处理指令,如
#include
、#define
等。 -
删除注释和多余空格。
-
生成预处理后的文件(通常以
.i
或.pre
为后缀)。
-
-
编译:
-
将预处理后的文件编译成汇编代码或目标代码。
-
生成汇编文件(以
.s
为后缀)或目标文件(以.o
或.obj
为后缀)。
-
-
汇编:
-
将汇编代码转换成机器代码。
-
生成目标文件(以
.o
或.obj
为后缀)。
-
-
链接:
-
将多个目标文件链接成一个可执行文件。
-
解决外部符号引用,生成可执行文件(以
.exe
为后缀)。
-
宏的优缺点?C++中会使用什么技术去替代宏?
宏的优点:
-
灵活性:宏可以在编译时展开,生成所需的代码。
-
性能:宏展开后的代码在编译时生成,运行时不需要额外的函数调用开销。
宏的缺点:
-
调试困难:宏展开后的代码可能会使调试变得复杂。
-
类型安全:宏不进行类型检查,可能导致类型错误。
-
作用域问题(命名冲突):宏在预处理阶段展开,可能导致意外的命名冲突。
在C++中
-
内联函数:使用
inline
关键字声明的函数,编译器会在调用处展开函数体,类似于宏。 -
模板:提供了一种泛型编程的方式,可以在编译时生成所需的代码,同时保持类型安全。
关键字
volatile
-
作用:用于声明一个变量的值可能会被程序之外的因素改变,例如硬件、操作系统或其他线程。这个关键字告诉编译器不要对这个变量进行优化,每次使用这个变量时都必须从其实际位置读取。
volatile int sensorValue;
extern
-
作用:用于声明一个变量或函数是在另一个文件中定义的,这样可以在当前文件中访问其他文件中定义的全局变量或函数。
extern int globalVar; // 声明在其他文件中定义的全局变量
static
-
作用:用于声明一个变量或函数的作用域仅限于声明它的文件或函数体内部。对于全局变量,
static
会限制其链接属性为内部链接(internal linkage),使其只能在定义它的文件中访问。static int localStaticVar; // 只能在定义它的函数内部访问
const
-
作用:用于声明一个变量的值在初始化后不能被改变,即常量。
const
可以用于变量、函数参数和函数返回值,表示数据的不可变性。const int MAX_VALUE = 100; // 不能改变MAX_VALUE的值
typedef
-
作用:用于为一个数据类型创建一个新的别名,简化复杂类型的声明,提高代码的可读性。
typedef unsigned int uint; uint var = 10; // 使用新的别名uint
sizeof
-
作用:用于计算一个表达式、类型或变量的大小(以字节为单位)。
int a = 10; printf("Size of a: %zu\n", sizeof(a)); // 输出变量a的大小
什么是指针?什么是数组?数组和指针的联系和区别?
指针:
-
指针是一个变量,它的值是另一个变量的内存地址。
-
指针可以用来间接访问和操作内存中的数据。
数组:
-
数组是存储在连续内存空间中的相同类型元素的集合。
-
数组名实际上是指向数组第一个元素的指针。
数组和指针的联系:
-
数组名可以作为指针使用,它指向数组的第一个元素。
-
通过指针运算可以访问数组中的元素。
数组和指针的区别:
-
数组是一个具体的数据结构,而指针是一个变量,用于存储地址。
-
数组的大小是固定的,而指针可以指向任何类型的数据。
-
数组有长度限制,而指针可以指向更大的内存区域。
什么是指针数组?什么是数组指针?
指针数组:
-
指针数组是一个数组,其元素都是指针。
-
指针数组的每个元素都指向一个变量或一个数据结构。
数组指针:
-
数组指针是一个指针,它指向一个数组。
-
通过数组指针可以访问数组中的所有元素。
&
数组名和数组名分别代表的意义?
-
&
数组名:表示数组的地址,即数组第一个元素的地址。 -
数组名:表示数组第一个元素的地址,也可以表示整个数组。
什么是二维数组?二级指针?
二维数组:
-
二维数组是一个数组,其元素是数组,通常用于表示矩阵或表格。
-
二维数组可以看作是数组的数组。
二级指针:
-
二级指针是一个指针,它指向一个指针。
-
二级指针可以用来间接访问指针所指向的内存地址。
什么是函数指针?
-
函数指针是一个指针,它指向一个函数。
-
通过函数指针可以调用函数,实现函数的动态调用和回调机制。