一:整型在内存中的存储
在 C 语言中,整型数据使用固定长度的字节来存储。通常情况下,int 类型占用 4 个字节(32位),其存储方式有 3 种,包括原码、反码和补码。
- 原码:
原码是最直观的表示方式,它将整数直接转换为二进制,并在最高位(最左边的位)上标识正负号。对于正数,最高位为0;对于负数,最高位为1。
例如:
- 整数75的原码表示为:00000000 00000000 00000000 01001011,
- 而整数-75的原码表示为:10000000 00000000 00000000 01001011
他们的区别就在最高的符号位上
- 反码:
反码的表示方式中,正数的反码与原码相同,而负数的反码是在原码的基础上,除了最高位之外,其它位按位取反。
例如:
- 整数75的反码表示为:00000000 00000000 00000000 01001011
- 对于75的反码表示为:11111111 11111111 11111111 10110100。
对于 75 来说,原码补码相同,而对于 -75 来说则需要进行转换了
- 补码:
正数的补码与原码相同,而负数的补码是在反码的基础上加1。
例如:
- 整数75的补码表示为:00000000 00000000 00000000 01001011
- 对于-75的补码表示为:11111111 11111111 11111111 10110101。
总结:
-
对于正数来说,正数的原码,反码,补码都相同
-
对于负数来说,负数的原码,反码,补码不相同
负数的原码,反码,补码转化如图所示:
举个例子:求一下 int a = 198 和 int b = -289 在内存的原码反码和补码
对于 int a = 198 来说,它的原码反码和补码都相同,都是
- 00000000 00000000 00000000 11000110
对于int b = -289来说,
- 原码: 10000000 00000000 00000001 00100001
- 反码: 11111111 11111111 11111110 11011110
- 补码: 11111111 11111111 11111110 11011111
注意:数据通过读直接写出来的是原码,但是数据在内存中存储的是补码
二:大小端介绍
- 大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
- 小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中。
那什么是数据的低位呢?
三:浮点型在内存中的存储
在 c 语言中,我们对浮点型在内存中的存储采用 IEEE754标准,任意一个二进制浮点数可以表示成下面的形式:
- (-1)^S * M * 2^E
对于这个形式我们进行解释:
-
(-1)^S表示符号位,当 S = 0,V 为正数;当 S = 1,V为负数。
-
M表示有效数字, 1≤ M <2 ,也就是说,M可以写成 1.xxxxxx 的形式,其中xxxxxx表示小数部分。
-
^E表示指数位。
IEEE 754规定:
对于 32 位的浮点数,最高的 1 位是符号位 s,接着的 8 位是指数 E,剩下的 23 位为有效数字 M。
对于 64 位的浮点数,最高的 1 位是符号位 S,接着的 11 位是指数E,剩下的 52 位为有效数字 M。
IEEE 754对有效数字M和指数E,还有一些特别规定。
3.1 数字 M:
前面说过, 1 ≤ M < 2 ,也就是说,M 可以写成 1.xxxxxx 的形式,其中 xxxxxx 表示小数部分。
IEEE 754 规定,在计算机内部保存 M 时,因为这个数的第一位总是 1,因此可以被舍去,只保存后面的 xxxxxx 部分。( 可以认为 M 就是 xxxxxx 部分 ) 比如保存 1.01 的时候,只保存 01,等到读取的时候,再把第一位的 1 加上去。
3.2 指数 E
至于指数 E,情况就比较复杂:首先,E 为一个无符号整数,这意味着,如果 E 为 8 位,它的取值范围为 0 ~~ 255 ;如果 E 为 11 位,它的取值范围为 0 ~ 2047。
但是,我们知道,科学计数法中的 E 是可以出现负数的,所以 IEEE 754 规定,存入内存时 E 的真实值必须再加上一个中间数, 对于 8 位的 E,这个中间数 是 127;对于 11 位的 E,这个中间数是 1023。
比如,2^10 的 E 是 10,所以保存成 32 位浮点数时,必须保存成 10+127=137,即 10001001。
3.2.1 进一步细分
然后,指数E从内存中取出还可以再分成三种情况:
- E 不全为 0 或不全为 1
这时,浮点数就采用这种规则表示:即指数 E 的计算值减去127(或1023),得到真实值,再将有效数字 M 前加上第一位的 1。
比如:0.5 的二进制形式为 0.1,由于规定正数部分必须为1,即将小数点右移 1 位,则为 1.0*2^(-1),其阶码为 -1+127=126,表示为 01111110,
而尾数 1.0 去掉整数部分变为 0 ,补齐 0 到 23 位00000000000000000000000,则 0.5 的二进制表示形式为: 0 01111110 00000000000000000000000
- E 全为 0
这时,浮点数的指数 E 等于 1-127(或者1-1023)即为真实值,有效数字 M 不再加上第一位的 1,而是还原为 0.xxxxxx 的小数。这样做是为了表示 ±0,以及接近于 0 的很小的数字。
- E全为1
这时,如果有效数字 M 全为 0,表示±无穷大(正负取决于符号位s);
下面通过一道例题巩固知识:将 20.59375 转换为IEEE754二进制表达方式,首先 20 转换为 2 进制应该是 10100,接着我们对小数部分进行转换:
所以小数部分就应该是 10011,所以 20.59375 可以转换为 10100.10011 -> (-1)^0 x 1.010010011 x 2^4,即 s = 0 ,e =4+127 = 131 ,m = 010010011(省略了1),所以完整的二进制应该是:
- 0 10000011 01001001100000000000000
转成 16 进制表示即:41A4C000
四:const 和 assert
4.1 assert
在 C 语言中,assert
是一个预处理宏,它用于检查程序中的某个条件是否为真,如果为假,则会终止程序的执行并输出相应的错误消息。
下面是assert
的基本语法:
#include <assert.h>
void assert(int expression);
assert
宏接受一个表达式作为参数,如果该表达式为假,程序会被终止,并输出包含错误信息的消息。如果表达式为真,则不会有任何影响,程序继续执行。
示例:
#include <assert.h>
int divide(int a, int b) {
assert(b != 0); // 确保分母不为零
return a / b;
}
上述代码中,如果b
为0,则断言失败,程序会终止执行,并输出错误消息。
4.2 const
在 C 语言中,const
关键字用于声明一个常量,也就是一个不可修改的变量。它可以应用于不同的上下文中,如变量声明、函数参数、函数返回类型等。
代码示例:
int main() {
const int x = 5; // x 声明为常量,并赋值为 5
x = 10; // 错误,无法修改常量的值
const int* p = &x; // p 是指向常量的指针,无法通过 p 修改 x 的值
int* const q = &x; // q 是一个常量指针,无法修改q的值,即无法改变指针指向
return 0;
}
我们定义了一个指向常量的指针 p
和一个常量指针 q
。无法通过指针 p
修改 x
的值,而 q
无法改变指向
那么,const int* p
和int *const p
之间有什么区别呢?
const int* p
:表示p
是一个指向常量的指针。指针本身可被修改,但不能通过指针修改所指向地址的值。int *const p
:表示p
是一个常量指针。指针本身是一个常量,不可被修改,但可以通过指针修改所指向地址的值。
简而言之,const int* p
允许修改指针本身,但不能通过指针修改值;int *const p
允许修改所指向地址的值,但不能修改指针本身。
五:字符函数和字符串函数
5.1 strlen strcpy strcat strcmp
让我们逐个介绍这些 C 语言中的字符串处理函数。
- strlen 函数:
strlen 函数用于计算字符串的长度,即字符串中的字符数(不包括空字符 ‘\0’)。
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "Hello, World!";
int length = strlen(str);
printf("字符串的长度为:%d\n", length);
return 0;
}
运行结果:
字符串的长度为 13。
注意:
- strlen(地址)括号中放的是地址
- 参数指向的字符串必须要以 ‘\0’ 结束(虽然不计算\0,但是需要 \0 这个标志)
- 函数的返回值为size_t,是无符号的( 易错 )
- strcpy 函数:
strcpy 函数用于将一个字符串复制到另一个字符串中,包括字符串末尾的空字符 ‘\0’。
#include <stdio.h>
#include <string.h>
int main() {
char source[] = "Hello, World!";
char destination[20];
strcpy(destination, source);
printf("复制后的字符串为:%s\n", destination);
return 0;
}
运行结果:
复制后的字符串为 "Hello, World!"。
注意:
- 源字符串必须以 ‘\0’ 结束。
- 会将源字符串中的 ‘\0’ 拷贝到目标空间。
- 目标空间必须足够大且可变,以确保能存放源字符串。
- strcat 函数:
strcat 函数用于将一个字符串连接到另一个字符串的末尾,结果是一个新的字符串。
#include <stdio.h>
#include <string.h>
int main() {
char str1[] = "Hello, ";
char str2[] = "World!";
strcat(str1, str2);
printf("连接后的字符串为:%s\n", str1);
return 0;
}
运行结果:
连接后的字符串为 "Hello, World!"。
注意:
- 源字符串必须以 ‘\0’ 结束。
- 目标空间必须有足够的大且可修改,能容纳下源字符串的内容。
- 不能自己给自己追加,即strcat(str, str);
- strcmp 函数:
strcmp 函数用于比较两个字符串是否相等,或者判断哪个字符串的大小顺序在前。
#include <stdio.h>
#include <string.h>
int main() {
char str1[] = "Hello";
char str2[] = "Hello";
int result = strcmp(str1, str2);
if (result == 0) {
printf("字符串相等\n");
} else if (result < 0) {
printf("字符串1在前\n");
} else {
printf("字符串2在前\n");
}
return 0;
}
运行结果:
字符串相等。
标准规定:
- 第一个字符串大于第二个字符串,则返回大于 0 的数字
- 第一个字符串等于第二个字符串,则返回 0
- 第一个字符串小于第二个字符串,则返回小于 0 的数字
5.2 strncpy strncat strncmp
strncpy
函数:
strncpy
函数用于将一个字符串的指定长度复制到另一个字符串中。它的函数原型如下:
char* strncpy(char* dest, const char* src, size_t n);
dest
:目标字符串,用于接收复制后的结果。src
:源字符串,要复制的字符串。n
:要复制的最大字符数。
strncpy
函数将 src
中的前 n
个字符复制到 dest
中,如果 src
的长度小于 n
,则会在 dest
的剩余位置上用空字符填充。
下面是一个使用 strncpy
函数的示例:
#include <stdio.h>
#include <string.h>
int main() {
char source[] = "Hello, World!";
char destination[20];
strncpy(destination, source, 5);
destination[5] = '\0'; // 手动添加字符串结束符
printf("Copied string: %s\n", destination);
return 0;
}
上述代码将 source
字符串的前 5 个字符复制到 destination
字符串中,并手动在第 6 个位置添加了字符串结束符。
strncat
函数:
strncat
函数用于将一个字符串的指定长度追加到另一个字符串的末尾。它的函数原型如下:
char* strncat(char* dest, const char* src, size_t n);
dest
:目标字符串,要追加的字符串将会添加到这里。src
:源字符串,要追加的字符串。n
:要追加的最大字符数。
strncat
函数将 src
中的前 n
个字符追加到 dest
的末尾,并确保 dest
仍然以空字符结尾。
下面是一个使用 strncat
函数的示例:
#include <stdio.h>
#include <string.h>
int main() {
char destination[20] = "Hello, ";
strncat(destination, "World!", 6);
printf("Concatenated string: %s\n", destination);
return 0;
}
上述代码将 "World!"
字符串的前 6 个字符追加到 destination
字符串的末尾。
strncmp
函数:
strncmp
函数用于比较两个字符串的指定长度。它的函数原型如下:
int strncmp(const char* str1, const char* str2, size_t n);
str1
:要进行比较的第一个字符串。str2
:要进行比较的第二个字符串。n
:要比较的最大字符数。
strncmp
函数按照字典顺序比较 str1
和 str2
的前 n
个字符。如果两个字符串相等返回 0,如果 str1
小于 str2
返回负数,如果 str1
大于 str2
返回正数。
下面是一个使用 strncmp
函数的示例:
#include <stdio.h>
#include <string.h>
int main() {
char str1[] = "Hello";
char str2[] = "Hey";
int result = strncmp(str1, str2, 3);
if (result == 0) {
printf("Strings are equal.\n");
} else if (result < 0) {
printf("str1 is less than str2.\n");
} else {
printf("str1 is greater than str2.\n");
}
return 0;
}
上述代码比较了 str1
和 str2
的前 3 个字符,并根据比较结果输出相应的信息。
5.3 strstr strtok
- strstr 函数:
strstr 函数用于在一个字符串中搜索另一个子字符串的出现位置。
函数原型:char *strstr(const char *str1, const char *str2)
- 参数 str1 是要搜索的字符串,str2 是要查找的子字符串。
- strstr 函数返回第一次出现 str2 的位置的指针,如果找不到则返回 NULL。
以下是一个代码示例:
#include <stdio.h>
#include <string.h>
int main() {
char str1[] = "Hello, world!";
char str2[] = "world";
char *result;
result = strstr(str1, str2);
if (result != NULL) {
printf("'%s' found at position %ld.\n", str2, result - str1);
} else {
printf("'%s' not found.\n", str2);
}
return 0;
}
输出结果为:'world' found at position 7.
- strtok 函数:
strtok 函数用于将一个字符串拆分成一系列子字符串,以指定的分隔符为准。
函数原型:char *strtok(char *str, const char *delim)
- 参数 str 是要拆分的字符串
- delim 是分隔符。
第一个参数又可以分为两种情况:
- strtok 函数的第一个参数不为 NULL ,函数将找到 str 中第一个标记,strtok 函数将保存它在字符串中的位置。
- strtok 函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记。
strtok 函数返回每次调用的子字符串指针,如果没有更多子字符串则返回 NULL,并在原字符串中用 ‘\0’ 替换分隔符。
以下是一个代码示例:
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "Hello, world! How are you today?";
for (char* token = strtok(str, " ,!"); token != NULL; token = strtok(NULL, " ,!")) {
printf("%s\n", token);
}
return 0;
}
输出结果为:
5.4 strerror perror
5.4.1 sterror
#include <stdio.h>
#include <string.h>
#include <errno.h>//必须包含的头文件
int main()
{
FILE* pFile;
pFile = fopen("unexist.ent", "r");
if (pFile == NULL)
printf("Error opening file unexist.ent: %s\n", strerror(errno));
//errno: Last error number
return 0;
}
errno 是 C 语言标准库中的一个全局变量,用于表示最近一次发生的错误代码。它在<errno.h> 头文件中定义。
在上述代码中,errno 用于获取 fopen 函数执行失败时的错误代码。strerror 函数用于将错误代码转换为对应的错误信息字符串。通过将 errno 作为参数传递给 strerror 函数,我们可以获取到错误原因的字符串。
如果文件打开失败,pFile 将为 NULL,并且通过使用 strerror(errno),你可以将错误信息打印到控制台。
5.4.2 perror
perror
函数是 C 语言标准库中的一个函数,用于打印与当前 errno 值关联的错误信息。它的原型如下:
void perror(const char *s);
函数参数 s
是一个指向字符串的指针,表示自定义的错误前缀。当 perror
函数被调用时,它会根据当前的 errno
值,在标准错误流 stderr
中打印错误信息,格式为 “[前缀]: [错误信息]”
下面是一个简单的示例代码,演示了如何使用 perror
函数:
#include <stdio.h>
#include <errno.h>
int main() {
FILE *file = fopen("nonexistent_file.txt", "r");
if (file == NULL) {
perror("Error");
return errno;
}
// 文件操作...
fclose(file);
return 0;
}
在这个示例中,我们尝试打开一个不存在的文件。由于打开失败,file
的值为 NULL
。接着使用 perror
函数打印错误信息,并返回 errno
值作为程序的退出码。
假设运行上述程序,输出可能类似于:
Error: No such file or directory
通过使用 perror
,我们不仅能够打印出错误消息如 “No such file or directory”,而且还能自定义前缀,如 “Error”。
5.5 字符操作函数
函数 | 如果他的参数符合下列条件就返回真 |
---|---|
iscntrl | 任何控制字符 |
isspace | 空白字符:空格‘ ’,换页‘\f’,换行’\n’,回车‘\r’,制表符’\t’或者垂直制表符’\v’ |
isdigit | 十进制数字 0 ~ 9 |
isxdigit | 十六进制数字,包括所有十进制数字,小写字母 a ~ f,大写字母 A ~ F |
islower | 小写字母 a ~ z |
isupper | 大写字母 A ~ Z |
isalpha | 字母 a ~ z 或 A ~ Z |
isalnum | 字母或者数字,a ~ z, A ~ Z,0 ~ 9 |
ispunct | 标点符号,任何不属于数字或者字母的图形字符(可打印) |
isgraph | 任何图形字符 |
isprint | 任何可打印字符,包括图形字符和空白字符 |
5.5 memcpy memmove memset memcmp
memcpy
、memmove
、memset
和 memcmp
这些函数都属于 <string.h>
头文件中的库函数。
memcpy
函数用于将一个内存区域的内容复制到另一个内存区域。其原型如下:
void *memcpy(void *dest, const void *src, size_t n);
其中,dest
是目标内存区域的指针,src
是源内存区域的指针,n
是要复制的字节数。该函数会将源内存区域的内容复制到目标内存区域,并返回目标内存区域的指针。
下面是一个示例代码,演示了如何使用 memcpy
函数:
#include <stdio.h>
#include <string.h>
int main() {
char source[] = "Hello, world!";
char destination[20];
memcpy(destination, source, strlen(source) + 1);
printf("Copied string: %s\n", destination);
return 0;
}
运行结果:
Copied string: Hello, world!
memmove
函数也用于将一个内存区域的内容复制到另一个内存区域,但与memcpy
不同,memmove
能够处理源内存区域和目标内存区域重叠的情况。其原型如下:
void *memmove(void *dest, const void *src, size_t n);
使用方法与 memcpy
类似。以下是一个示例代码:
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "12345";
memmove(str + 2, str, strlen(str) + 1);
printf("Moved string: %s\n", str);
return 0;
}
运行结果:
Moved string: 1212345
memset
函数用于将一块内存区域的所有字节设置为指定的值。其原型如下:
void *memset(void *s, int c, size_t n);
其中,s
是要设置的内存区域的起始地址,c
是要设置的值,n
是要设置的字节数。该函数会将指定内存区域的字节全部设置为指定值,并返回起始地址。
以下是一个示例代码,演示了如何使用 memset
函数:
#include <stdio.h>
#include <string.h>
int main() {
char str[10];
memset(str, 'A', sizeof(str));
printf("Result: %s\n", str);
return 0;
}
上述代码将字符数组 str
的所有字节都设置为 'A'
,并通过 printf
输出了结果。
memcmp
函数用于比较两个内存区域的内容。其原型如下:
int memcmp(const void *s1, const void *s2, size_t n);
其中,s1
和 s2
是要比较的两个内存区域的指针,n
是要比较的字节数。该函数以字节为单位逐个比较内存区域的内容
- 如果比较的结果相等,返回值为 0;
- 如果
s1
大于s2
,返回值大于 0; - 如果
s1
小于s2
,返回值小于 0。
以下是一个示例代码,演示了如何使用 memcmp
函数:
#include <stdio.h>
#include <string.h>
int main() {
char str1[] = "Hello";
char str2[] = "Hello";
int result = memcmp(str1, str2, sizeof(str1));
if (result == 0) {
printf("Strings are equal.\n");
} else {
printf("Strings are not equal.\n");
}
return 0;
}
运行结果:
Strings are equal.