C语言入门
小知识点
printf
: print format
%d
: decimal, 整数%s
: string, 常用于char* 和 string%p
: 输出地址, 常用于指针, &a%#x
: 0x十六进制%f
: 以十进制输出flaot, 默认保留6位小数, 不足补0, 超过截断四舍五入%lf
: double, 同上%e
: 以指数形式输出 float 类型,输出结果中的 e 小写;%g
: 以最短的方式来输出小数,并且小数部分表现很自然,不会强加零%c
: char类型
完整形式:
%[flag][width][.precision]type
例子:
%-9d
中,d
表示以十进制输出,9
表示最少占9个字符的宽度,宽度不足以空格补齐,-
表示左对齐。综合起来,%-9d表示以十进制输出,左对齐,宽度最小为9个字符%.9d
, 123456 -> 000123456
type
: 数据类型,如上
.precision
: 输出精度, 补 0
- 对于小数: 当小数部分的位数大于 precision 时,会按照四舍五入的原则丢掉多余的数字;
当小数部分的位数小于 precision 时,会在后面补 0 - 整数: 用于整数时,.precision 表示最小输出宽度。与 width 不同的是,整数的宽度不足时会在左边补 0,而不是补空格。
- 用于字符串时: .precision 表示最大输出宽度,或者说截取字符串。当字符串的长度大于 precision 时,会截掉多余的字符;当字符串的长度小于 precision 时,.precision 就不再起作用。
width
: 当输出结果的宽度不足 width 时,以空格补齐(如果没有指定对齐方式,默认会在左边补齐空格);当输出结果的宽度超过 width 时,width 不再起作用,按照数据本身的宽度来输出。
flag
: 标志字符, 例如,%#x
中 flag 对应 #
,%-9d
中 flags 对应-
。下表列出了 printf() 可以用的 flag:
标志字符 | 含 义 |
---|---|
- | - 表示左对齐。如果没有,就按照默认的对齐方式,默认一般为右对齐。 |
+ | 用于整数或者小数,表示输出符号(正负号)。如果没有,那么只有负数才会输出符号。 |
空格 | 用于整数或者小数,输出值为正时冠以空格,为负时冠以负号。 |
# |
|
scanf
: scan format- 对于 scanf(),输入数据的格式要和控制字符串的格式保持一致。
- 要把输入的数据存入内存, &a
- scanf() 的控制字符串为"%d, %d, %d",中间以逗号分隔,所以输入的整数也要以逗号分隔。
- scanf() 不会跳过不符合要求的数据,遇到不符合要求的数据会读取失败,而不是再继续等待用户输入。
行缓冲区
当遇到 scanf() 函数时,程序会先检查输入缓冲区中是否有数据:
- 如果没有,就等待用户输入。用户从键盘输入的每个字符都会暂时保存到缓冲区,直到按下回车键,产生换行符\n,输入结束,scanf() 再从缓冲区中读取数据,赋值给变量。
- 如果有数据,那就看是否符合控制字符串的规则:
- 如果能够匹配整个控制字符串,那最好了,直接从缓冲区中读取就可以了,就不用等待用户输入了。
- 如果缓冲区中剩余的所有数据只能匹配前半部分控制字符串,那就等待用户输入剩下的数据。
- 如果不符合,scanf() 还会尝试忽略一些空白符,例如空格、制表符、换行符等:
- 如果这种尝试成功(可以忽略一些空白符),那么再重复以上的匹配过程。只有当控制字符串以格式控制符开头时,才会忽略换行符
- 如果这种尝试失败(不能忽略空白符),那么只有一种结果,就是读取失败。
#include <stdio.h>
int main()
{
int a = 1, b = 2;
scanf("a=%d", &a);
scanf("b=%d", &b);
printf("a=%d, b=%d\n", a, b);
return 0;
}
若输入
a=99 b=200↙
a=99, b=2
则第二个scanf读取失败, 因为scanf("b=%d", &b);
中间的空格不能忽略.
-
%d
:当以有符号数的形式输出时,printf 会读取数字所占用的内存,并把最高位作为符号位,把剩下的内存作为数值位;
%u
: 当以无符号数的形式输出时,printf 也会读取数字所占用的内存,并把所有的内存都作为数值位对待。
可以说,“有符号正数的最高位是 0”这个巧合才使得 %o 和 %x 输出有符号数时不会出错。 -
请读者注意一个小细节,如果是
unsigned int
类型,那么可以省略int
,只写unsigned
-
小数和整数相互赋值
- 将一个整数赋值给小数类型,在小数点后面加 0 就可以,加几个都无所谓。
- 将一个小数赋值给整数类型,就得把小数部分丢掉,只能取整数部分,这会改变数字本来的值。注意是直接丢掉小数部分,而不是按照四舍五入取近似值。
-
char类型实际上存储的是整数,ASCII表;
如果输出%c
c 就是字符; 输出%d
c就是整数; c也可以是int类型一个字符在存储之前会转换成它在字符集中的编号,而这样的编号是一个整数,所以我们可以用整数类型来存储一个字符
-
C语言编码类型
对于 char 类型的窄字符,始终使用 ASCII 编码。
对于 char 类型的窄字符串,微软编译器使用本地编码,GCC、LLVM/Clang 使用和源文件编码相同的编码。
对于 wchar_t 类型的宽字符和宽字符串,使用 UTF-16 或者 UTF-32 编码,它们都是基于 Unicode 字符集的。 -
一些容易出错的优先级问题
优先级问题 | 表达式 | 经常误认为的结果 | 实际结果 |
---|---|---|---|
. 的优先级高于 *(-> 操作符用于消除这个问题) | *p.f | p 所指对象的字段 f,等价于: (*p).f | 对 p 取 f 偏移,作为指针,然后进行解除引用操作,等价于: *(p.f) |
[] 高于 * | int *ap[] | ap 是个指向 int 数组的指针,等价于: int (*ap)[] | ap 是个元素为 int 指针的数组,等价于: int *(ap []) |
函数 () 高于 * | int *fp() | fp 是个函数指针,所指函数返回 int,等价于: int (*fp)() | fp 是个函数,返回 int*,等价于: int* ( fp() ) |
== 和 != 高于位操作 | (val & mask != 0) | (val &mask) != 0 | val & (mask != 0) |
== 和 != 高于赋值符 | c = getchar() != EOF | (c = getchar()) != EOF | c = (getchar() != EOF) |
算术运算符高于位移 运算符 | msb << 4 + lsb | (msb << 4) + lsb | msb << (4 + lsb) |
逗号运算符在所有运 算符中优先级最低 | i = 1, 2 | i = (1,2) | (i = 1), 2 |
-
在函数内部定义的变量、数组、结构体、共用体等都称为局部数据。在很多编译器下,局部数据的初始值都是随机的、无意义的,而不是我们通常认为的“零”值。这一点非常重要,一定要谨记,否则后面会遇到很多奇葩的错误.
-
函数只能由一个返回值.
-
void 函数没有返回值. 但是可以
return;
表示结束函数.
所以, 也不能int a = [void] func()
; -
一个C语言程序的执行过程可以认为是多个函数之间的相互调用过程,它们形成了一个或简单或复杂的调用链条。这个链条的起点是
main()
,终点也是main()
。当main()
调用完了所有的函数,它会返回一个值(例如return 0;
)来结束自己的生命,从而结束整个程序。
从上面的分析可以推断出,在所有函数之外进行加减乘除运算、使用 if…else 语句、调用一个函数等都是没有意义的,这些代码位于整个函数调用链条之外,永远都不会被执行到。C语言也禁止出现这种情况,会报语法错误 -
在所有函数外部定义的变量称为全局变量(Global Variable),它的作用域默认是整个程序,也就是所有的源文件,包括 .c 和 .h 文件。但是仍然需要定义在函数之前.
-
C语言规定,只能从小的作用域向大的作用域中去寻找变量,而不能反过来,使用更小的作用域中的变量。
-
typedef
和#define
的区别
typedef
在表现上有时候类似于#define
,但它和宏替换之间存在一个关键性的区别。正确思考这个问题的方法就是把 typedef 看成一种彻底的“封装”类型,声明之后不能再往里面增加别的东西。
重要知识点
原码, 反码, 补码
参考链接
在计算机内存中,整数一律采用补码的形式来存储。这意味着,当读取整数时还要采用逆向的转换,也就是将补码转换为原码。将补码转换为原码也很简单:先减去 1,再将数值位取反即可。
为什么需要补码?
为了设计一种高效的电路, 不区分符号位, 数值位, 且同时实现加法和减法的运算.
反码缺陷
小数-大数
结果正确
大数-小数
结果小1
补码如何生效?
- 小数减去大数,结果为负数,之前(负数从反码转换为补码要加 1)加上的 1,后来(负数从补码转换为反码要减 1)还要减去,正好抵消掉,所以不会受影响。
- 而大数减去小数,结果为正数,之前(负数从反码转换为补码要加 1)加上的 1,后来(正数的补码和反码相同,从补码转换为反码不用减 1)就没有再减去,不能抵消掉,这就相当于给计算结果多加了一个 1。
正数
: 原码==反码==补码
负数
:
原码–>符号位为1 ;
反码: 除符号位外数值位全部取反;
补码: 反码+1
补码 | 反码 | 原码 | 值 |
---|---|---|---|
1111 1111 | 1111 1110 | 1000 0001 | -1 |
1111 1110 | 1111 1101 | 1000 0010 | -2 |
1111 1101 | 1111 1100 | 1000 0011 | -3 |
…… | …… | …… | …… |
1000 0011 | 1000 0010 | 1111 1101 | -125 |
1000 0010 | 1000 0001 | 1111 1110 | -126 |
1000 0001 | 1000 0000 | 1111 1111 | -127 |
1000 0000 | ---- | ---- | -128 |
0111 1111 | 0111 1111 | 0111 1111 | 127 |
0111 1110 | 0111 1110 | 0111 1110 | 126 |
0111 1101 | 0111 1101 | 0111 1101 | 125 |
…… | …… | …… | …… |
0000 0010 | 0000 0010 | 0000 0010 | 2 |
0000 0001 | 0000 0001 | 0000 0001 | 1 |
0000 0000 | 0000 0000 | 0000 0000 | 0 |
为什么最小值比最大值大1?
按照传统的规则,1000 0000
是无法计算的, 因为变成反码需要-1
,而最高位是符号位,不能借出去, 因此就直接规定该特殊的补码的值为-128
.
浮点数
计算机的设计是一门艺术, 很多实用技术都是权衡和妥协的结果.
相比, 深度学习是什么鬼?
IEEE754标准
- 符号位: 与int, short等一样, 0表示正数, 1表示负数
- 尾数部分: 当采用二进制形式后,尾数部分的取值范围为
1 ≤ mantissa < 2
,这意味着:尾数的整数部分一定为 1,是一个恒定的值,这样就无需在内存中提现出来,可以将其直接截掉,只要把小数点后面的二进制数字放入内存中即可。对于1.0011101
,就是把0011101
放入内存。 - 指数部分: 不采用符号位+补码形式(需要两次转换), 而是根据指数部分的取值范围确定一个中间值, 写入指数时加上这个中间值, 读出指数时减去这个中间值.
对于float
,中间值为 28-1 - 1 = 127;对于double
,中间值为 211-1 -1 = 1023。
规格化浮点数: 指数 exp 的所有二进制位既不全为 0 也不全为 1 时;
对于“正常”的浮点数,尾数 mant 隐含的整数部分为 1
,并且在读取浮点数时,内存中的指数 exp 要减去中间值 median 才能还原真实的指数 exponent,也即:
mantissa = 1.mant
exponent = exp - median
非规格化浮点数: 指数 exp 全为 0 时;
但是当指数 exp 的所有二进制位都为 0 时,一切都变了!尾数 mant 隐含的整数部分变成了 0,并且用 1 减去中间值 median 才能还原真实的指数 exponent,也即:
mantissa = 0.mant
exponent = 1 - median
对于 float
,exponent = 1 - 127 = -126
,指数 exponent 的值恒为 -126;对于 double
,exponent = 1 - 1023 = -1022
,指数 exponent 的值恒为 -1022。
另外, 对于非规格化浮点数, 当尾数 mant 的所有二进制位都为 0 时,整个浮点数的值就为 0:
如果符号 sign 为 0,则表示 +0
;
如果符号 sign 为 1,则表示 -0
;
特殊值: 指数 exp 全为 1 时;
- 如果此时尾数 mant 的二进制位都为 0,则表示无穷大:
- 如果符号 sign 为 1,则表示负无穷大;
- 如果符号 sign 为 0,则表示正无穷大。
- 如果此时尾数 mant 的二进制位不全为 0,则表示
NaN
(Not a Number),也即这是一个无效的数字,或者该数字未经初始化。
平滑过度⬇️
说明 | float 内存 | exp | exponent | mant | mantissa | 浮点数的值 flt |
---|---|---|---|---|---|---|
0值 最小非规格化数 最大非规格化数 | 0 - 00...00 - 00...00 0 - 00...00 - 00...01 0 - 00...00 - 00...10 0 - 00...00 - 00...11 …… 0 - 00...00 - 11...10 0 - 00...00 - 11...11 | 0 0 0 0 …… 0 0 | -126 -126 -126 -126 …… -126 -126 | 0 2^-23 2^-22 1.1 × 2^-22 …… 0.11...10 0.11...11 | 0 2^-23 2^-22 1.1 × 2^-22 …… 0.11...10 0.11...11 | +0 2^-149 2^-148 1.1 × 2^-148 …… 1.11...10 × 2^-127 1.11...11 × 2^-127 |
最小规格化数 最大规格化数 | 0 - 00...01 - 00...00 0 - 00...01 - 00...01 …… 0 - 00...10 - 00...00 0 - 00...10 - 00...01 …… 0 - 11...10 - 11...10 0 - 11...10 - 11...11 | 1 1 …… 2 2 …… 254 254 | -126 -126 …… -125 -125 127 127 | 0.0 0.00...01 …… 0.0 0.00...01 …… 0.11...10 0.11...11 | 1.0 1.00...01 …… 1.0 1.00...01 …… 1.11...10 1.11...11 | 1.0 × 2^-126 1.00...01 × 2^-126 …… 1.0 × 2^-125 1.00...01 × 2^-125 …… 1.11...10 × 2^127 1.11...11 × 2^127 |
0 - 11...11 - 00...00 | - | - | - | - | +∞ | |
0 - 11...11 - 00...01 …… 0 - 11...11 - 11...11 | - | - | - | - | NaN |
缓冲区
三种类型:
-
全缓冲:
在这种情况下,当缓冲区被填满以后才进行真正的输入输出操作。缓冲区的大小都有限制的,比如 1KB、4MB 等,数据量达到最大值时就清空缓冲区。 -
行缓冲:
在这种情况下,当在输入或者输出的过程中遇到换行符(\n
)时,才执行真正的输入输出操作。行缓冲的典型代表就是标准输入设备(也即键盘)和标准输出设备(也即显示器)。 -
不带缓冲:
不带缓冲区,数据就没有地方缓存,必须立即进行输入输出。
缓冲区的刷新(清空)
- 刷新缓冲区,就是将缓冲区中的内容送达到目的地。缓冲区的刷新遵循以下的规则:
- 不管是行缓冲还是全缓冲,缓冲区满时会自动刷新;
- 行缓冲遇到换行符\n时会刷新;
- 关闭文件时会刷新缓冲区;
- 程序关闭时一般也会刷新缓冲区,这个是由标准库来保障的;
- 函数,如下
清空缓冲区
fflush()
是一个专门用来清空缓冲区的函数,stdout
是 standard output 的缩写,表示标准输出设备,也即显示器。整个语句的意思是,清空标准输出缓冲区,或者说清空显示器的缓冲区。
fflush(stdout);
清空输入缓冲区
使用 getchar()
清空缓冲区
getchar()
是带有缓冲区的,每次从缓冲区中读取一个字符,包括空格、制表符、换行符等空白符,只要我们让 getchar() 不停地读取,直到读完缓冲区中的所有字符,就能达到清空缓冲区的效果。请看下面的代码:
int c;
while((c = getchar()) != '\n' && c != EOF);
数组赋值
当赋值的元素少于数组总体元素的时候,剩余的元素自动初始化为 0:
- 对于short、int、long,就是整数 0;
- 对于char,就是字符 ‘\0’; 在ASCII表中也是 0
- 对于float、double,就是小数 0.0。
所以想要把数组全赋值为0, 可
int nums[10] = {0};
char str[10] = {0};
float scores[10] = {0.0};
字符数组和字符串
在C语言中, 没有string类型, 通常用一个字符数组来存放字符串.
char str[30] = {"c.biancheng.net"};
char str[] = "c.biancheng.net"; //这种形式更加简洁,实际开发中常用
字符串结束标志(划重点
在C语言中,字符串总是以'\0'
作为结尾,所以'\0'
也被称为字符串结束标志,或者字符串结束符。
有如下特点:
-
'\0'
是 ASCII 码表中的第 0 个字符,英文称为 NUL,中文称为“空字符”。该字符既不能显示,也没有控制功能,输出该字符不会有任何效果,它在C语言中唯一的作用就是作为字符串结束标志。 -
由
" "
包围的字符串会自动在末尾添加'\0'
-
需要注意的是,逐个字符(for 循环)地给数组赋值并不会自动添加
'\0'
,char str[] = {‘a’, ‘b’, ‘c’};
-
当用字符数组存储字符串时,要特别注意
'\0'
,要为'\0'
留个位置;这意味着,字符数组的长度至少要比字符串的长度大 1。char str[7] = “abc123”;
-
数组长度
string.h
头文件中的strlen()
函数, 不包括最后的结束符'\0'
.
字符串常用函数
- 字符串连接函数 strcat()
strcat 是 string catenate 的缩写,意思是把两个字符串拼接在一起,语法格式为:
strcat(arrayName1, arrayName2);
strcat() 将把 arrayName2 连接到 arrayName1 后面,并删除原来 arrayName1 最后的结束标志'\0'
。这意味着,arrayName1 必须足够长,要能够同时容纳 arrayName1 和 arrayName2,否则会越界(超出范围)。
- 字符串复制函数 strcpy()
strcpy 是 string copy 的缩写,意思是字符串复制,也即将字符串从一个地方复制到另外一个地方,语法格式为:
strcpy(arrayName1, arrayName2);
连同 '\0'
一起拷贝
- 字符串比较函数 strcmp()
strcmp 是 string compare 的缩写,意思是字符串比较,语法格式为:
strcmp(arrayName1, arrayName2);
字符本身没有大小之分,strcmp() 以各个字符对应的 ASCII 码值进行比较。strcmp() 从两个字符串的第 0 个字符开始比较,如果它们相等,就继续比较下一个字符,直到遇见不同的字符,或者到字符串的末尾。
返回值:若 arrayName1 和 arrayName2 相同,则返回0
;若 arrayName1 大于 arrayName2,则返回大于 0
的值;若 arrayName1 小于 arrayName2,则返回小于0
的值。
数组feature
-
静态数组
在C语言中,数组一旦被定义后,占用的内存空间就是固定的,容量就是不可改变的,既不能在任何位置插入元素,也不能在任何位置删除元素,只能读取和修改元素,我们将这样的数组称为静态数组。 -
数组越界
C语言为了提高效率,保证操作的灵活性,并不会对越界行为进行检查,即使越界了,也能够正常编译,只有在运行期间才可能会发生问题。 -
数组溢出
当赋予数组的元素个数超过数组长度时,就会发生溢出(Overflow)。一般情况下数组溢出不会有什么问题,顶多是丢失多余的元素。但是当以字符串的形式输出字符数组时,就会产生不可控的情况. y -
变长数组
普通数组(固定长度的数组)是在编译期间分配内存的,而变长数组是在运行期间分配内存的。
int n;
scanf("%d", &n);
int arr[n];
注意, 以下不是变长数组, 它的长度由未初始化的局部变量 n 决定, 其值是未知的
int n;
int arr[n];
scanf("%d", &n);
递归
- 尾递归: 递归发生在函数的结尾. 如求阶乘,
return factorial(n - 1) * n;
- 中间递归: 它在进入和退出递归的两个阶段都会进行一些操作
- 多层递归: 多层递归的调用关系比较复杂,整体上看起来像一颗倒立的树:对于双层递归,树的每个节点有两个分叉;对于三层递归,树的每个节点有三个分叉;以此类推……
缺点 空间, 时间开销非常大
空间: 递归容易导致Stack溢出
在程序占用的整个内存中,有一块内存区域叫做栈(Stack),它是专门用来给函数分配内存的,每次调用函数,都会将相关数据压入栈中,包括局部变量、局部数组、形参、寄存器、冗余数据等。
时间: 内存分配和释放, 修改寄存器的值…
如何解决: 利用 迭代 代替 递归
预处理命令
以 #
号开头的命令称为预处理命令
预处理主要是处理以 #
开头的命令,例如 #include <stdio.h>
等。预处理命令要放在所有函数之外,而且一般都放在源文件的前面。
预处理是C语言的一个重要功能,由预处理程序完成。当对一个源文件进行编译时,系统将自动调用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。
- #include
//不同的平台下引入不同的头文件
#include <stdio.h>
//不同的平台下引入不同的头文件
#if _WIN32 //识别windows平台
#include <windows.h>
#elif __linux__ //识别linux平台
#include <unistd.h>
#endif
int main() {
//不同的平台下调用不同的函数
#if _WIN32 //识别windows平台
Sleep(5000);
#elif __linux__ //识别linux平台
sleep(5);
#endif
puts("http://c.biancheng.net/");
return 0;
}
- #define
#define
叫做宏定义命令,它也是C语言预处理命令的一种。所谓宏定义,就是用一个标识符来表示一个字符串,如果在后面的代码中出现了该标识符,那么就全部替换成指定的字符串。简单粗暴的字符串替换- 宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也一起替换。
- 可用宏定义表示数据类型,使书写方便。例如
#define UINT unsigned int
- 宏在编译之前就被处理掉了,它没有机会参与编译,也不会占用内存
条件编译: 能够根据不同情况编译不同代码、产生不同目标文件的机制, 是预处理程序的功能, 不是编译器的功能!
- #if
#if 整型常量表达式1
程序段1
#elif 整型常量表达式2
程序段2
#elif 整型常量表达式3
程序段3
#else
程序段4
#endif
谁真编译谁! 只能是整型常量表达式, : 0
假, 非 0
真
- #ifdef
#ifdef 宏名
程序段1
#else
程序段2
#endif
如果当前的宏已被定义过,则对“程序段1”进行编译,否则对“程序段2”进行编译。
- #ifndef
与上述相反, 同样只针对宏.
- #error
#error 指令用于在编译期间产生错误信息,并阻止程序的编译,其形式如下:
#ifndef __cplusplus
#error 当前程序必须以C++方式编译
#endif
结构体
-
结构体
结构体是一种自定义的数据类型,是创建变量的模板,不占用内存空间;结构体变量才包含了实实在在的数据,需要内存空间来存储。
结构体变量名和数组名不同,数组名在表达式中会被转换为数组指针,而结构体变量名不会,无论在任何表达式中它表示的都是整个集合本身,要想取得结构体变量的地址,必须在前面加&
, 如:
struct stu *pstu = &stu1;
但是, 当结构体指针作为函数参数
结构体变量名代表的是整个集合本身,作为函数参数时传递的整个集合,也就是所有成员,而不是像数组一样被编译器转换成一个指针。如果结构体成员较多,尤其是成员为数组时,传送的时间和空间开销会很大,影响程序的运行效率。所以最好的办法就是使用结构体指针,这时由实参传向形参的只是一个地址,非常快速。很神奇, 这时候又不用
&
struct stu{ char *name; //姓名 int num; //学号 int age; //年龄 char group; //所在小组 float score; //成绩 }stus[] = { {"Li ping", 5, 18, 'C', 145.0}, }; void average(struct stu *ps, int len); int main(){ int len = sizeof(stus) / sizeof(struct stu); average(stus, len); return 0; } ...
为结构体定义别名
typedef struct stu{ char name[20]; int age; char sex; } STU;
STU body1,body2;
==struct stu body1, body2;
-
枚举enum
枚举和宏其实非常类似:宏在预处理阶段将名字替换成对应的值,枚举在编译阶段将名字替换成对应的值。我们可以将枚举理解为编译阶段的宏。 这意味着,Mon、Tues、Wed 等都不是变量,它们不占用数据区(常量区、全局数据区、栈区和堆区)的内存,而是直接被编译到命令里面,放到代码区,所以不能用&
取得它们的地址。这就是枚举的本质 -
共用体union
共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。#include <stdio.h> union data{ //占用4个字节 int n; char ch; short m; }; int main(){ union data a; printf("%d, %d\n", sizeof(a), sizeof(union data) ); a.n = 0x40; printf("%X, %c, %hX\n", a.n, a.ch, a.m); a.ch = '9'; printf("%X, %c, %hX\n", a.n, a.ch, a.m); a.m = 0x2059; printf("%X, %c, %hX\n", a.n, a.ch, a.m); a.n = 0x3E25AD54; printf("%X, %c, %hX\n", a.n, a.ch, a.m); return 0; }
大端小端
- 大端模式(Big-endian) 是指将数据的低位(比如 1234 中的 34 就是低位)放在内存的高地址上,而数据的高位(比如 1234 中的 12 就是高位)放在内存的低地址上。这种存储模式有点儿类似于把数据当作字符串顺序处理,地址由小到大增加,而数据从高位往低位存放。 单片机, ARM等常用大端模式.
- 小端模式(Little-endian) 是指将数据的低位放在内存的低地址上,而数据的高位放在内存的高地址上。这种存储模式将地址的高低和数据的大小结合起来,高地址存放数值较大的部分,低地址存放数值较小的部分,这和我们的思维习惯是一致,比较容易理解。现代CPU常用小端模型.
以 int
类型的 0x12345678
为例,它占用 4 个字节,如果是小端模式(Little-endian),那么在内存中的分布情况为(假设从地址 0x 4000 开始存放):
内存地址 | 0x4000 | 0x4001 | 0x4002 | 0x4003 |
---|---|---|---|---|
存放内容 | 0x78 | 0x56 | 0x34 | 0x12 |
如果是大端模式(Big-endian),那么分布情况正好相反:
内存地址 | 0x4000 | 0x4001 | 0x4002 | 0x4003 |
---|---|---|---|---|
存放内容 | 0x12 | 0x34 | 0x56 | 0x78 |
const 和 指针
-
常量指针(指向常量的指针): 指针所指向的数据是只读的,也就是
p1
、p2
本身的值可以修改(指向不同的数据),但它们指向的数据不能被修改。const int *p1;
int const *p2; -
指针常量(指针是常量): 指针是只读的,也就是
p3
本身的值不能被修改, 但p3
所指向内存地址所对应的值是可以修改的,因为其并不是const类型, 因此在声明的同时对其进行初始化.int * const p3;
记忆: const
离变量名近就是用来修饰指针变量(p
)的,离变量名远就是用来修饰指针指向的数据(int
),如果近的和远的都有,那么就同时修饰指针变量以及它指向的数据
生成随机数
#include <stdio.h>
#include <stdlib.h> // srand() 设定随机数种子
#include <time.h> //time()
int main() {
int a;
srand((unsigned)time(NULL));
a = rand(); //产生 0 ~ RAND_MAX 之间的整数
printf("%d\n", a);
return 0;
}
指针
一切都是地址
C语言用变量来存储数据,用函数来定义一段可以重复使用的代码,它们最终都要放到内存中才能供 CPU 使用。
数据和代码都以二进制的形式存储在内存中,计算机无法从格式上区分某块内存到底存储的是数据还是代码。当程序被加载到内存后,操作系统会给不同的内存块指定不同的权限,拥有读取和执行权限的内存块就是代码,而拥有读取和写入权限(也可能只有读取权限)的内存块就是数据。
CPU 只能通过地址来取得内存中的代码和数据,程序在执行过程中会告知 CPU 要执行的代码以及要读写的数据的地址。如果程序不小心出错,或者开发者有意为之,在 CPU 要写入数据时给它一个代码区域的地址,就会发生内存访问错误。这种内存访问错误会被硬件和操作系统拦截,强制程序崩溃,程序员没有挽救的机会。
CPU 访问内存时需要的是地址,而不是变量名和函数名!变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,它们都会被替换成地址。编译和链接过程的一项重要任务就是找到这些名称所对应的地址。
- 定义
*
是一个特殊符号,表明一个变量是指针变量,定义p1
、p2
时必须带*
。而给p1
、p2
赋值时,因为已经知道了它是一个指针变量,就没必要多此一举再带上*
,后边可以像使用普通变量一样来使用指针变量。也就是说,定义指针变量时必须带*
,给指针变量赋值时不能带*
。这个解释好像还挺好的!
- 使用
指针变量存储了数据的地址,通过指针变量能够获得该地址上的数据,格式为*pointer;
这里的
*
称为指针运算符,用来取得某个地址上的数据, 与定义时的*
含义完全不同!
例子:
//定义普通变量
float a = 99.5, b = 10.6;
char c = '@', d = '#';
//定义指针变量
float *p1 = &a;
char *p2 = &c;
//修改指针变量的值
p1 = &b;
p2 = &d;
从结果上看, 两种方式都是输出a的值, 但是通过指针是间接获取数据, 通过变量名这是直接获取
int a = 15;
int *p = &a;
printf("%d, %d\n", a, *p); //两种方式都可以输出a的值
return 0;
指向数组首元素
int arr[] = { 99, 15, 100, 888, 252 };
int *p = arr;
此时, arr
, p
, &arr[0]
三者地址相同.
字符数组(char str[])和字符串常量(char *str)
根本区别
最根本的区别是在内存中的存储区域不一样,字符数组存储在全局数据区或栈区,第二种形式的字符串存储在常量区。
全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,而常量区的字符串(也包括其他数据)只有读取权限,没有写入权限。
因此charr *不可以修改字符串中的字符! 但是可以修改指针的指向
#include <stdio.h>
int main(){
char *str = "Hello World!";
str = "I love C!"; //正确
str[3] = 'P'; //错误
return 0;
}
用数组作函数参数
#include <stdio.h>
int max(int *intArr, int len){
int i, maxValue = intArr[0]; //假设第0个元素是最大值
for(i=1; i<len; i++){
if(maxValue < intArr[i]){
maxValue = intArr[i];
}
}
return maxValue;
}
int main(){
int nums[6], i;
int len = sizeof(nums)/sizeof(int);
//读取用户输入的数据并赋值给数组元素
for(i=0; i<len; i++){
scanf("%d", nums+i);
}
printf("Max value is %d!\n", max(nums, len));
return 0;
}
实际上也只是传递了数组中的首地址而已, 切记, 这个不是数组指针, 因此函数内部不知道数组的长度, sizeof(intArr) == 8
, 而在main
函数中, sizeof(nums) == 24(4*6)
.
在形参中, int *arr == int arr[] == int arr[num]
为什么不给传递整个数组?
参数的传递本质上是一次赋值的过程,赋值就是对内存进行拷贝。所谓内存拷贝,是指将一块内存上的数据复制到另一块内存上。 因此数组的拷贝可能花费较多时间.
指针作为返回值
指针作为返回值时, 最好不要指向函数局部数据, 例如包括局部变量、局部数组和形式参数等等, 因为函数运行结束后会销毁在它内部定义的所有局部数据 后续内存待补充
NULL指针及void指针
-
一般NULL是C标准库中的宏定义, 在进程的虚拟地址空间中,最低地址处有一段内存区域被称为保留区,这个区域不存储有效数据,也不能被用户程序访问,将 NULL 指向这块区域很容易检测到违规指针。所以
NULL
大部分指向 地址0
#define NULL ((void *)0)
-
void指针
void *
表示一个有效指针,它确实指向实实在在的数据,只是数据的类型尚未确定,在后续使用过程中一般要进行强制类型转换。
C语言动态内存分配函数malloc()
的返回值就是void *
类型,在使用时要进行强制类型转换,请看下面的例子:
#include <stdio.h>
int main(){
//分配可以保存30个字符的内存,并把返回的指针转换为 char *
char *str = (char *)malloc(sizeof(char) * 30);
gets(str);
printf("%s\n", str);
return 0;
}
指针和数组
C语言标准规定,当数组名作为数组定义的标识符(也就是定义或声明数组时)、sizeof
或 &
的操作数时,它才表示整个数组本身,在其他的表达式中,数组名会被转换为指向第 0 个元素的指针(地址)。
为什么要这样
如果数组名只表示数组的集合, 直接使用集合没有明确的含义, 将数组名转化为指向数组的指针后, 才可以很容易地访问其中的任意一份数据.
对数组的引用 a[i] 在编译时总是被编译器改写成*(a+i)的形式,C语言标准也要求编译器必须具备这种行为。