C语言学习笔记

小知识点

  1. 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

例子:

  1. %-9d中,d表示以十进制输出,9表示最少占9个字符的宽度,宽度不足以空格补齐,-表示左对齐。综合起来,%-9d表示以十进制输出,左对齐,宽度最小为9个字符
  2. %.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:

标志字符 含  义
- -表示左对齐。如果没有,就按照默认的对齐方式,默认一般为右对齐。
+ 用于整数或者小数,表示输出符号(正负号)。如果没有,那么只有负数才会输出符号。
空格 用于整数或者小数,输出值为正时冠以空格,为负时冠以负号。
#
  • 对于八进制(%o)和十六进制(%x / %X)整数,# 表示在输出时添加前缀;八进制的前缀是 0,十六进制的前缀是 0x / 0X。
  • 对于小数(%f / %e / %g),# 表示强迫输出小数点。如果没有小数部分,默认是不输出小数点的,加上 # 以后,即使没有小数部分也会带上小数点。
  1. 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); 中间的空格不能忽略.

  1. %d:当以有符号数的形式输出时,printf 会读取数字所占用的内存,并把最高位作为符号位,把剩下的内存作为数值位;
    %u: 当以无符号数的形式输出时,printf 也会读取数字所占用的内存,并把所有的内存都作为数值位对待。
    可以说,“有符号正数的最高位是 0”这个巧合才使得 %o 和 %x 输出有符号数时不会出错。

  2. 请读者注意一个小细节,如果是unsigned int类型,那么可以省略 int ,只写 unsigned

  3. 小数和整数相互赋值

    • 将一个整数赋值给小数类型,在小数点后面加 0 就可以,加几个都无所谓。
    • 将一个小数赋值给整数类型,就得把小数部分丢掉,只能取整数部分,这会改变数字本来的值。注意是直接丢掉小数部分,而不是按照四舍五入取近似值。
  4. char类型实际上存储的是整数,ASCII表;
     如果输出%c c 就是字符; 输出%d c就是整数; c也可以是int类型

    一个字符在存储之前会转换成它在字符集中的编号,而这样的编号是一个整数,所以我们可以用整数类型来存储一个字符

  5. C语言编码类型

    对于 char 类型的窄字符,始终使用 ASCII 编码。
    对于 char 类型的窄字符串,微软编译器使用本地编码,GCC、LLVM/Clang 使用和源文件编码相同的编码。
    对于 wchar_t 类型的宽字符和宽字符串,使用 UTF-16 或者 UTF-32 编码,它们都是基于 Unicode 字符集的。

  6. 一些容易出错的优先级问题

优先级问题 表达式 经常误认为的结果 实际结果
. 的优先级高于 *(-> 操作符用于消除这个问题) *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
  1. 在函数内部定义的变量、数组、结构体、共用体等都称为局部数据。在很多编译器下,局部数据的初始值都是随机的、无意义的,而不是我们通常认为的“零”值。这一点非常重要,一定要谨记,否则后面会遇到很多奇葩的错误.

  2. 函数只能由一个返回值.

  3. void 函数没有返回值. 但是可以 return; 表示结束函数.
    所以, 也不能 int a = [void] func();

  4. 一个C语言程序的执行过程可以认为是多个函数之间的相互调用过程,它们形成了一个或简单或复杂的调用链条。这个链条的起点是 main(),终点也是 main()。当 main() 调用完了所有的函数,它会返回一个值(例如return 0;)来结束自己的生命,从而结束整个程序。
    从上面的分析可以推断出,在所有函数之外进行加减乘除运算、使用 if…else 语句、调用一个函数等都是没有意义的,这些代码位于整个函数调用链条之外,永远都不会被执行到。C语言也禁止出现这种情况,会报语法错误

  5. 在所有函数外部定义的变量称为全局变量(Global Variable),它的作用域默认是整个程序,也就是所有的源文件,包括 .c 和 .h 文件。但是仍然需要定义在函数之前.

  6. C语言规定,只能从小的作用域向大的作用域中去寻找变量,而不能反过来,使用更小的作用域中的变量。

  7. typedef#define 的区别
    typedef 在表现上有时候类似于 #define ,但它和宏替换之间存在一个关键性的区别。正确思考这个问题的方法就是把 typedef 看成一种彻底的“封装”类型,声明之后不能再往里面增加别的东西。

重要知识点

原码, 反码, 补码

参考链接
在计算机内存中,整数一律采用补码的形式来存储。这意味着,当读取整数时还要采用逆向的转换,也就是将补码转换为原码。将补码转换为原码也很简单:先减去 1,再将数值位取反即可。

为什么需要补码?
为了设计一种高效的电路, 不区分符号位, 数值位, 且同时实现加法和减法的运算.
反码缺陷
小数-大数 结果正确
大数-小数 结果小1
补码如何生效?

  • 小数减去大数,结果为负数,之前(负数从反码转换为补码要加 1)加上的 1,后来(负数从补码转换为反码要减 1)还要减去,正好抵消掉,所以不会受影响。
  • 而大数减去小数,结果为正数,之前(负数从反码转换为补码要加 1)加上的 1,后来(正数的补码和反码相同,从补码转换为反码不用减 1)就没有再减去,不能抵消掉,这就相当于给计算结果多加了一个 1。

正数: 原码==反码==补码
负数:
    原码–>符号位为1 ;
    反码: 除符号位外数值位全部取反;
    补码: 反码+1

补码反码原码
1111 11111111 11101000 0001-1
1111 11101111 11011000 0010-2
1111 11011111 11001000 0011-3
……………………
1000 00111000 00101111 1101-125
1000 00101000 00011111 1110-126
1000 00011000 00001111 1111-127
1000 0000---------128
0111 11110111 11110111 1111127
0111 11100111 11100111 1110126
0111 11010111 11010111 1101125
……………………
0000 00100000 00100000 00102
0000 00010000 00010000 00011
0000 00000000 00000000 00000

为什么最小值比最大值大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

缓冲区

三种类型:

  1. 全缓冲:
    在这种情况下,当缓冲区被填满以后才进行真正的输入输出操作。缓冲区的大小都有限制的,比如 1KB、4MB 等,数据量达到最大值时就清空缓冲区。

  2. 行缓冲:
    在这种情况下,当在输入或者输出的过程中遇到换行符(\n)时,才执行真正的输入输出操作。行缓冲的典型代表就是标准输入设备(也即键盘)和标准输出设备(也即显示器)。

  3. 不带缓冲:
    不带缓冲区,数据就没有地方缓存,必须立即进行输入输出。

缓冲区的刷新(清空)

  • 刷新缓冲区,就是将缓冲区中的内容送达到目的地。缓冲区的刷新遵循以下的规则:
  • 不管是行缓冲还是全缓冲,缓冲区满时会自动刷新;
  • 行缓冲遇到换行符\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'也被称为字符串结束标志,或者字符串结束符。
有如下特点:

  1. '\0'是 ASCII 码表中的第 0 个字符,英文称为 NUL,中文称为“空字符”。该字符既不能显示,也没有控制功能,输出该字符不会有任何效果,它在C语言中唯一的作用就是作为字符串结束标志。

  2. " "包围的字符串会自动在末尾添加'\0'

  3. 需要注意的是,逐个字符(for 循环)地给数组赋值并不会自动添加'\0',

    char str[] = {‘a’, ‘b’, ‘c’};

  4. 当用字符数组存储字符串时,要特别注意'\0',要为'\0'留个位置;这意味着,字符数组的长度至少要比字符串的长度大 1。

    char str[7] = “abc123”;

  5. 数组长度 string.h 头文件中的 strlen() 函数, 不包括最后的结束符 '\0' .

字符串常用函数

  1. 字符串连接函数 strcat()
    strcat 是 string catenate 的缩写,意思是把两个字符串拼接在一起,语法格式为:

strcat(arrayName1, arrayName2);

strcat() 将把 arrayName2 连接到 arrayName1 后面,并删除原来 arrayName1 最后的结束标志'\0'。这意味着,arrayName1 必须足够长,要能够同时容纳 arrayName1 和 arrayName2,否则会越界(超出范围)。

  1. 字符串复制函数 strcpy()
    strcpy 是 string copy 的缩写,意思是字符串复制,也即将字符串从一个地方复制到另外一个地方,语法格式为:

strcpy(arrayName1, arrayName2);

连同 '\0' 一起拷贝

  1. 字符串比较函数 strcmp()
    strcmp 是 string compare 的缩写,意思是字符串比较,语法格式为:

strcmp(arrayName1, arrayName2);

字符本身没有大小之分,strcmp() 以各个字符对应的 ASCII 码值进行比较。strcmp() 从两个字符串的第 0 个字符开始比较,如果它们相等,就继续比较下一个字符,直到遇见不同的字符,或者到字符串的末尾。
返回值:若 arrayName1 和 arrayName2 相同,则返回0;若 arrayName1 大于 arrayName2,则返回大于 0 的值;若 arrayName1 小于 arrayName2,则返回小于0 的值。

数组feature

  1. 静态数组
    在C语言中,数组一旦被定义后,占用的内存空间就是固定的,容量就是不可改变的,既不能在任何位置插入元素,也不能在任何位置删除元素,只能读取和修改元素,我们将这样的数组称为静态数组

  2. 数组越界
    C语言为了提高效率,保证操作的灵活性,并不会对越界行为进行检查,即使越界了,也能够正常编译,只有在运行期间才可能会发生问题。

  3. 数组溢出
    当赋予数组的元素个数超过数组长度时,就会发生溢出(Overflow)。一般情况下数组溢出不会有什么问题,顶多是丢失多余的元素。但是当以字符串的形式输出字符数组时,就会产生不可控的情况. y

  4. 变长数组
    普通数组(固定长度的数组)是在编译期间分配内存的,而变长数组是在运行期间分配内存的。

int n;
scanf("%d", &n);
int arr[n];

注意, 以下不是变长数组, 它的长度由未初始化的局部变量 n 决定, 其值是未知的

int n;
int arr[n];
scanf("%d", &n);

递归

  1. 尾递归: 递归发生在函数的结尾. 如求阶乘, return factorial(n - 1) * n;
  2. 中间递归: 它在进入和退出递归的两个阶段都会进行一些操作
  3. 多层递归: 多层递归的调用关系比较复杂,整体上看起来像一颗倒立的树:对于双层递归,树的每个节点有两个分叉;对于三层递归,树的每个节点有三个分叉;以此类推……

缺点 空间, 时间开销非常大

空间: 递归容易导致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 开始存放):

内存地址0x40000x40010x40020x4003
存放内容0x780x560x340x12

如果是大端模式(Big-endian),那么分布情况正好相反:

内存地址0x40000x40010x40020x4003
存放内容0x120x340x560x78

const 和 指针

  • 常量指针(指向常量的指针): 指针所指向的数据是只读的,也就是 p1p2 本身的值可以修改(指向不同的数据),但它们指向的数据不能被修改。

    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 访问内存时需要的是地址,而不是变量名和函数名!变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,它们都会被替换成地址。编译和链接过程的一项重要任务就是找到这些名称所对应的地址。


  • 定义
    * 是一个特殊符号,表明一个变量是指针变量,定义 p1p2 时必须带*。而给 p1p2 赋值时,因为已经知道了它是一个指针变量,就没必要多此一举再带上*,后边可以像使用普通变量一样来使用指针变量。也就是说,定义指针变量时必须带*,给指针变量赋值时不能带*

    这个解释好像还挺好的!

  • 使用
    指针变量存储了数据的地址,通过指针变量能够获得该地址上的数据,格式为 *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语言标准也要求编译器必须具备这种行为。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值