C语言系统级编程

荣老师太帅了aaaa!!(成熟男人的魅力谁懂😭)

为什么开这门课?

”基础不牢,地动山摇“

这门课的目标

能够自己理解C语言标准中的内容

  1. 属于规则的一般化
  2. 概念组织的系统化

介绍C语言标准背后的设计思想

引例:C语言中有没有变量(variable)

1
2
3
int a = 0; //芝士什么? 变量
const inst b = 0; //芝士什么? 常量?? 常量也是变量???
const volatile int c = 0; //芝士什么? (无法解释)

引例2:常量 vs. 常量表达式

形式 名称
10 整数常量
+10 常量表达式

现有学习资料和标准的概念体系有一定差异

  1. 核心概念介绍缺失
    对象、lvalue、rvalue等核心概念介绍不详细或者不涉及
  2. 部分概念混淆有误
    变量、常量的划分方法,常量和常量表达式的混淆等
  3. 存在许多自创概念
    常变量、只读变量、行指针、列指针、数组指针、指针数组、多级指针、模拟引用传递

标准一般化为什么重要?

都是语法糖,没有意义?会用就行了?

如果使用变量术语,只有int a = 0;是变量,其它定义均不知所谓
实际上,它们都是“变量标识符”

标准中的逻辑规则强调的就是一般化

一般化的基础:以对象为核心构建概念体系

  1. 对象的基本概念(对象的各种属性)
  2. 如何分配一个对象(4种分配方式)
  3. 如何定位一个对象(17类表达式)
  4. 如何获得lvalue和non-lvalue表达式的相关信息
  5. 如何释放对象占用的内存

本课程内容简介

C语言知识类别

  1. 语法类知识
  2. 概念类知识
  3. 策略类知识

学习周期和考核方法

学习时长1-9周(16学时,1学分)

考核方法:考试(开卷,带什么资料都行)+随堂测验(上课的时候带张草稿纸)


(正片开始)

内存基本概念简单回顾

1byte=8bits?

<limits.h>定义了一个宏CHAR_BIT

C语言规定一个byte等于CHAR_BIT个bit

CHAR_BIT的限制是>=8(即不一定是8位)

地址是字节在内存中的编号,只有字节才有地址

C语言最重要的概念:对象

  1. 对象的概念
  2. 如何刻画对象的属性
  3. 理解对象该类别
  4. 理解指针和数组也是一种普通的对象类型

对象表示(Object Representation)

  • 一个对象各个bit组成的二进制串就是这个对象的对象表示
  • 对象表示$\leftarrow$对象类型$\rightarrow$对象的值

对象表示 vs. 对象的值

  • 对象表示一样,对象的值一定一样
  • 对象的值一样,对象表示不一定一样

对象类型T

C语言任何一个对象类型都可以形式化定义为一个T

  • sizeof(T)
    规定了任何一个对象包含的字节个数,n为size,sizeof(T)=n * CHAR_BIT
  • alignof(T)
    对象地址(Address):对象所占用的连续字节中lowest字节的编号
    对齐要求(Alignment):要求对象的地址能被对齐要求整除

所有的算数类型和指针类型统称为标量类型,只有标量类型支持加减操作

对象分类——算术类型(Arithmetic Type)

C23正式增加bool类型

算术类型都是完全对象类型(即size确定)

char、signed char、unsigned char是三种不同的类型,编译器会指定char和另外两种中的一个表现相同

  • 标准有符号整数类型对象

1个sign bit 若干value bit 若干padding bit

signed char、signed short int、signed int、signed long int、signed long long int

  • 标准无符号整数类型对象

若干value bit 若干padding bit

unsigned char、unsigned short int、unsigned int、unsigned long int、unsigned long long int、unsigned bool

typedef

tepedef是一个能够为类型设置别名的关键字

1
typedef T alias

typedef可以提高系统移植能力

1
2
3
4
5
#ifdef ImplementationA
typedef int INT32;
#elifdef ImplementationB
typedef int INT16;
#endif

用法:

1
2
3
typedef T alias; //非数组非指针
typedef T Alias[N]; //数组类型
typedef T* Alias; //指针类型

对齐要求

对象对齐要求一定是$2^n$

n越小越weak,越大约strong

枚举类型

  1. 在枚举定义时指定类型

    1
    enum Season
  2. 不指定类型,自动指定为char、标准有符号类型、标准无符号类型、扩展有符号证书类型、扩展无符号整数类型的一种

对象分类——派生类型(Derived Type)

重点介绍数组类型和指针类型

数组类型

形式化定义:T[N]

  • T:元素类型,必须是完全对象类型
  • N:元素类型的个数

N为表达式:

  1. 无N,不完全对象类型
  2. 有N,N为整数常量或整数常量表达式,普通数组类型
  3. 有N,不为整数常量或整数常量表达式,变长数组类型

数组类型也是一个合法的对象类型,有大小和对齐要求

typeof

数组类型可以视为一个整体,即:

1
typedef int AINT[5];

由于写法过于恶臭(确实),C23提供了一个关键字

1
2
typedef typeof(int[5]) AINT;
// typeof T Alias;

从数组类型T继续派生

由于数组类型也为完全对象类型,所以仍可继续派生

于是出现了二维数组!

1
2
3
4
int a[10][5];
typedef int AINT[5];
typedef AINT AAINT[10][5];
AAINT a;

数组都是“一维”的

指针类型

形式化定义:T*

  • T:Referenced Type:对象类型或函数类型
  • *:指针类型标志

T*为从T派生的类型

注意:T包括完全函数类型,也包括不完全对象类型,还包括函数类型

指针类型是完全对象类型

sizeof(T*)

alignof(T*)

指针类型再派生

指针类型也能用于构造数组类型,指针类型也有自己的指针类型

类型 派生数组 派生指针
T T[N] T*
T[M] T[N][M] T(*)[M]
T* T*[N] T**

你已经学会了,现在为int*(**[])[4][5]派生指针类型吧
答案:int*(**(*)[])[4][5]

限定类型/限定符(Qualifier)

const volatile restrict

若T为类型,Q为限定符。当T为一个整体的时候,限定符在T的左边右边都一样(T Q = Q T)

1
2
3
4
5
Q T = T Q //非数组非指针
Q T[N] //数组类型,元素类型Q T
T[N] Q //不合法
Q T*//类型为Q T
T* Q //Q限制T*类别 <-------推荐的

理解const int* const

  1. const int,限定类型,限定int

  2. const int*,指针类型,Referenced Type为const int

  3. const int* const,限定类型,限定const int*

Q typeof(T[N]) = Q typeof(T)[N]

  • 多个不同限定符限定同一个类型等价
  • 多个同样限定符限定同一个类型等价于一个

认识对象的属性

{A, Obj_T, N, S, V, V_T, Align}

  • 对象名称 T
    有名称的对象被称为具名对象(Named Object)
  • 大小 Size
    sizeof(T)
  • 地址
    最低位地址
  • 对齐要求
    对齐要求:alignof(T)
  • 对象值和对象表示
    之前讲过了喵(善用ctrl+f
  • 表示值 Value
  • 表示类型 Value Type

<表示值,表示值类型>

非数组对象类型:<object value,typeof_unqual(object Type)>

数组对象类型:<第一个元素对象的地址,元素类型对应的指针类型>

分配对象:通过对象声明

分配对象的方法

rwg:建议一次只声明一个对象🤓

对象声明

  • 基本形式:T O = initalizer;
  • T应当看作一个整体,即typeof(T)
  • int(*d)[10]; = typeof(int(*)[10]) d;

String Literal

String Literal是由编码前缀双引号引导的字符序列构成的

编码前缀包括:

编码前缀 字符类型 编码
无编码 \ \
u8 char8_t UTF-8
u char16_t UTF-16
U char32_t UTF-32
L wchar_t 实现定义

字符序列中的字符包括:

  1. 所有char,除了" \ 实际的回车
  2. \引导的转义字符

步骤(以初始化“hello”为例):

  1. 在内存中分配一个大小合适的char类型数组对象
  2. 用’h’,‘e’,‘l’,‘l’,‘o’,'\0’初始化数组的每一个元素
  3. 该对象为匿名对象
  4. 当第二次初始化“hello”时,不一定需要分配一个新的地址空间(如"hello"和L"hello")

String Literal vs. String

双引号内不包含"\0"的String Literal叫做String,String Literal包含String

Compound Literal

内存管理函数

malloc

  1. 分配一个匿名对象,该对象无对象类型
  2. 大小为size
  3. 对象表示为indeterminate
  4. 返回值为空指针(未成功)或者这个对象的首地址
  5. 该对象的Storage Duration是allocated
  6. 对齐要求为Fundamental Alignment,保证了返回的void*指针可以强转成任意类型

initalizier

初始化对象的对象表示

标量类型initiallizer形式:

  • {},空初始化列表
    • 将对象的值设置为该类型的缺省值
  • 除逗号表达式之外的任意表达式epx或{exp}

数组类型initiallizer形式:

  • {},空初始化列表
    • 将对象的值递归设置为该类型的缺省值
  • {sub-initalizer1,…}
    • 设置前n个对象,剩下设置为缺省值

当对变长数组初始化时,只能使用{}进行初始化

没有initializer时,对象值初始化为indeterminate representation(不确定值)

对象值、对象表示一一对应

表达式

C语言中,定位一个对象,只能通过lvalue(locate value)

  • 表达式(Exp)由一些列符号和操作数组成
  • 共有17种表达式

Evaluation of Expression

  • Evaluate的过程包括
    1. Value Computation(计算值)
    2. Initiation of Side Effect(确定副作用)
  • Value Computation得到表达式的rvalue,包含值和类型

基础表达式(Primary Expression)

标识符:包括对象标识符和函数标识符

  • 对象声明 T O = Initializer,其中O就是对象标识符
  • 对象标识符是合法的lvalue
  • Value Computation:非数组——对象值,数组——第一个元素首地址
  • Side Effect:无

常量

  • 非左值,无法定位对象

  • 整数常量、浮点数常量、枚举常量、字符常量、预定义常量

  • 整数常量:进制前缀+数字序列+类型后缀

    • 前缀

      前缀 含义
      0b/0B 二进制
      十进制
      0 八进制
      0x/0X 十六进制
    • 后缀(重点)

      敲不动了,放个图

      从上至下为类型匹配顺序
      不考虑BitInt系列,整数常量的类型,最小是int(涉及到整型提升的概念)

    • 数字分隔符不能出现在第一个数字前,不能出现在最后一个数字后

  • 浮点数常量:进制前缀+符号序列+后缀

    • 十进制符号序列形式:1.1e-3
      • 科学计数部分不一定有
    • 十六进制符号序列形式:0x100.5p2
      • 幂的底数为2,指数部分为十进制
  • 枚举常量,比较简单

  • 字符常量:编码前缀+单引号引导的有效字符

    • 前缀
      当没有给定前缀时,系统会默认类型,所以中文字符在一些系统会变成乱码

      字符常量 rvalue类型
      ‘a’ int
      u8’a’ char8_t
      u’a’ char16_t
      U’a’ char32_t
      L’a’ wchar_t
    • 有效字符

      • 基本源代码字符集
      • 基本执行字符集
      • 扩展源代码字符集、扩展执行字符集
    • ‘\x31’ :以十六进制整数表示的字符,十进制值是49,依然是字符’1’

  • 预定义常量:false、true、nullptr

    • false和true分别为0和1,rvalue类型为bool
    • nullptr是一个空指针类型,rvalue类型为在<stddef.h>中定义的nullptr_t类型

字符串

字符串不是常量

  • 字符串可以用来分配(或重用)一个对象
  • 同时,字符串是一个lvalue,能够定位分配出来(或重用)的那个对象

字符串分配(或重用)的对象类型是字符数组类型

如果lvalue定位的对象是一个数组类型对象,则lvalue表达式Evaluate之后的rvalue就是该对象第一个元素的首地址,rvalue类型是元素对象类型对应的指针类型

括号表达式

evaluate的优先级最高,rvalue和类型等于括号内表达式的值

后缀表达式

给定一个表达式exp,与后缀操作符结合构成的仍然是一个表达式

后缀操作符:[]、.、->、++/–、(type-name){Initializer-list}、()

一元表达式

给定一个表达式exp,与一元操作符结构构成的仍然是一个表达式

一元操作符:++/–、&、*、+/-、~/!、sizeof、_Alignof

evaluate

定位非数组对象lvalue的evaluate

对形如如下形式的lvalue不做evaluate,表达式整体evaluate由特殊规则确定

  • sizeof(a),编译时确定
  • typeof(a),编译时确定
  • &a,在main外时称为地址常量表达式
  • a++/a–,rvalue为a的rvalue加1或减1,同时将a的value改变(有副作用,可能两个不同时发生)
  • ++a/–a,rvalue为a的rvalue加1或减1,同时将a的value改变(有副作用,可能两个不同时发生)
  • a=2,rvalue为等号右边表达式的rvalue,同时将左值的value改变(有副作用,可能两个不同时发生)

常量不可修改的原因是左值为不可修改左值