Skip to content

czh01 C语言入门教程

学习方法

  • coding and parsing
  • 滚动式学习,通过后续知识的学习复习巩固前面的知识
  • 通过实践(编程)学习掌握 C 标准,而不是通过学习 C 标准来指导实践(编程)

资源链接


章节目录

1 hello world

1.1 hello.c

vi/vscode 编辑器编写第一个程序 hello.c:

c
#include <stdio.h>

int main()
{
        printf("hello, world\n");
        return 0;
}
  • 第 1 行:以 # 开头的行都由预处理器处理,预处理器将 stdio.h 文件的内容复制到我们的文件中。xxx.h 文件称为预处理头文件,通常包含函数声明。 stdio.h 文件中包含函数 printf(...)的声明。
  • 第 3 行: main 函数,程序开始执行的入口点。int 表示 main() 的返回类型。() 表示 main 函数不带任何参数。
  • 第 4 和 7 行:{} 一对大括号定义了函数体,也叫做复合语句块。所有函数都必须以大括号开头和结尾。
  • 第 5 行: printf() 是一个标准库函数,用于在标准输出上打印一些东西。末尾的 ; 用来表示语句的结束。
  • 第 6 行: return 0;, 返回语句,值 0 通常表示成功终止。

1.2 预处理,编译,汇编,链接和运行

1.2.1 gcc 编译器集合

  • 预处理 Preprocess 将 stdio.h 文件的内容复制到 hello.c
    stdio.h + hello.c => hello.c
  • 编译 Compile 将C语言源文件翻译为汇编语言
    hello.c -> hello.s
  • 汇编 Assembly 由汇编源文件生成机器指令二进制代码
    hello.s -> hello.o
  • 链接 Linking 将二进制对象文件链接成可执行文件
    hello.o + printf.o => a.out

1.2.2 终端命令行

bash
$ gcc hello.c     # create a.out
$ ./a.out
hello, world
$

1.3 文件格式,进程地址空间

  • ELF可执行文件 view wiki
    ELF = ELF header + text section + data section
  • 进程地址空间
    address space = text + data + stack
    • text 文本段(指令)
    • data 数据段
    • stack

2 数据存储和C语言

2.1 数据存储格式


2.1.1 数字系统

number system

  • 十进制 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
  • 二进制 0, 1
  • 二进制-十进制转换表
  • 十进制到二进制转换
  • N位二进制最大值

    do (2) - (1)





2.1.2 位,字节和内存

bit, byte and memory

  • bit 二进制数字,01,物理上低电平表示 0,高电平表示 1
voltLEDbinary
lowoff0
highon1
  • byte 8bit 位组合形成一个字节,物理上是一个内存单元格(cell)
    数据表示范围: 0 - 255
    binary: 1010 1101
    decimal: 173
    A cell:

    10101101
    173
  • memory 连续的单元格(byte)形成的长数组,由起始为0的数字进行编号寻址


2.1.3 ASCII 编码

ASCII: 美国信息交换标准代码  扩展阅读 wiki
ASCII 码是 7 位码,其格式(排列)为

需要 1 byte 来存储

需要熟悉的 ASCII 编码

binarydeccharcomment
0000 10019\t制表键
0000 101010\n换行键
0010 000032SP空格键
0011 00004809: 57
0100 000165AZ: 90
0110 000197az: 122

例:键盘输入 Mary
M   0100 1101 (77)
a   0110 0001 (97)
r   0111 0010 (114)
y   0111 1001 (121)

内存的两种表示:

Mary
7797114121

2.1.4 内存支持的位组

  • byte   1-byte
  • word   2-bytes
  • double-word   4-bytes
  • quad-word   8-bytes
namebytesunsigned rangesigned range
byte1 ~ ~
word2 ~ ~
dword4 ~ ~
qword8 ~ ~

2.2 数据类型和变量

data type and variable


2.2.1 数据类型

C 语言提供了下列几种基本数据类型

keyworddata typesizerange
charinteger1 to
shortinteger2 to
intinteger4 to
longinteger8 to
floatsingle float4signed
doubledouble float8signed

无符号整型:
unsigned char, unsigned short, unsigned int, unsigned long


2.2.2 变量

  • 内存地址的人类可读名称
  • 关联内存地址易于记忆的标签
  • 存储、读取和更改内存中的数据
  • 总是同具体的数据类型相关联

2.2.3 变量命名规则

  • 字符数字下划线组成
  • 首字符不能是数字
  • 不能与C关键字冲突

2.2.4 变量的声明

变量在使用前必须进行声明,这是通过声明语句完成的。

c
int main()
{
	int x;
	int y;
	int m, n;

	return 0;
}

行尾的分号表示一条语句的结束。
变量的内存空间由编译器在 stack 中自动分配(auto)。


2.3 运算符和表达式

operator and expression


2.3.1 赋值运算符 =

asignment operator

  • 赋值语句
    x = 28;
    int n = 3523;

  • printf 格式化输出函数
    printf("x = %d\n", x);

    % - 转换说明符

    • %dint 类型,十进制数字
    • %uunsigned int 类型, 无符号十进制数
    • %cint 类型,单个字符
    • %fdouble 类型, 十进制小数
    • %schar * 类型,顺序打印字符串中的字符
    • %pvoid * 类型;指针(十六进制地址)

code example
c
/* 
 * file name: prog2_1.c
 */

#include <stdio.h>

int main()
{
        int x;
        int y;

        x = 28;
        y = 3523;
        printf("x = %d\n", x);
        printf("y = %d\n", y);
        printf("x = %d\ty=%d\n", x, y);
        return 0;
}

2.3.2 算术运算符,表达式

算术运算符属于二元运算符(有左、右两个操作数的运算符) binary operator

  • 算术运算符
    +, -, *, /, %
  • 表达式:运算符和操作数形成表达式 expression
    expr = operand op ...
  • 语句:表达式后跟一个 ; 形成语句 statement
    stmt = expr ;
  • 合法的表达式和语句
    • 3 + 4;
    • i = x * y;
    • i = i + x * y;
  • 表达式的值
    C语言表达式被翻译为一条或多条汇编指令,由CPU顺序执行,最终的结果被保存在CPU寄存器中,这称为表达式的值。

2.3.3 赋值运算符 op=

大多数二元运算符都有一个相应的赋值运算符 op=
expr1 op= expr2; <=> expr1 = expr1 op expr2
例如:

  • i += 6; <=> i = i + 6;
  • i *= x + 6; <=> i = i * (x+6)

code example
c
#include <stdio.h>

int main()
{
        int x, i;

        i = 3;
        x = 5;
        i *= x + 6;
        printf("i = %d\n", i);
        return 0;
}

2.3.4 ++, -- 运算符

属于一元运算符(只有一个操作数) unary operator
一元运算符拥有最高的优先级

  • prefix ++ --
    ++i;
    y = 10 + --i;
    y = ++i * 2;

  • postfix ++ --
    i++;
    y = 10 + i--;
    y = i-- * 2;


2.3.5 相等运算符

equality operator == !=

  • x == y 1 if x equals y, otherwise 0
  • x != y 1 if x does not equals y, otherwise 0

code example
c
#include <stdio.h>

int main()
{
        int x, y, z;
        
        x = 10;
        y = 10;
        z = x == y;
        printf("x = %d\ty=%d\tz = %d\n", x, y, z);
        x++;
        z = x == y;
        printf("x = %d\ty=%d\tz = %d\n", x, y, z);
        z = x != y;
        printf("x != y\tz = %d\n", z);
        return 0;
}

2.3.6 关系运算符

relational operator > >= < <=

  • x > y   1 if x greater than y, otherwise 0
  • x >= y   1 if x greater or equal y, otherwise 0
  • x < y   1 if x is less than y, otherwise 0
  • x <= y   1 if x less or equal y, otherwise 0

2.3.7 逻辑运算符

logical operator && ||

  • x && y   1 if x != 0 and y != 0, otherwise 0
  • x || y   1 if x != 0 or y != 0, otherwise 0

2.3.8 一元运算符

unary operator ! &

  • ! logical NOT, 运算结果是 01
    !8;   0
    !0;   1
    !x;   1 if x == 0; otherwise 0

  • & 取地址运算符
    &x; /* get address of variable x */
    printf("%p\n", &x); /* p - pointer, print address */
    scanf("%d", &x); 这里 scanf 是标准输入格式化函数,%d说明符接收终端十进制数字输入并存入变量x中,&x说明传递给scanf的是变量x的地址。


code example
c
/*
 * calculate rectangle's area
 */
 
#include <stdio.h>

int main()
{
        int a, b, s;       /* width, length, area */
        
        printf("input width and length: ");
        scanf("%d %d", &a, &b);
        s = a * b;
        printf("The rectangle's area is: %d\n\n", s);
        printf("Address of var a: %p\n", &a);
        return 0;
}

3 控制流

flow control

  • 普通语句以 ; 结尾,包括声明语句和表达式语句
  • 复合/块语句,以 { 开头, 以 } 结尾, 最后不需要分号;块语句中包括一条或多条声明语句和表达式语句

3.1 if-else 语句

if-else 语句用于条件判断,语法格式为

c
if (expr)
        statement1
else  /* optional */
        statement2

if-else 语句执行逻辑:

  1. 计算 expr
    • 如果其值为真(非 0),则执行 statement1
    • 如果存在 else 子句,并且 expr 值为假(0),则执行 statement2
  2. 继续执行后面的程序代码

code example
c
/*
 * get max from two absolute value
 */
 
#include <stdio.h>

int main()
{
        int x1, x2, max;
    
        printf("input x1 and x2: ");
        scanf("%d %d", &x1, &x2);
        if (x1 < 0)
                x1 = -x1;
        if (x2 < 0)
                x2 = -x2;
        if (x1 >= x2)
                max = x1;
        else
                max = x2;
        printf("The max value is: %d\n", max);
        return 0;
}

编码风格:

coding style

if (expr)            if (expr != 0)
---------------------------------------------
if (0.2)             if (0.2 != 0)
---------------------------------------------
if (i && j)          if (i != 0 && j != 0)
--------------------------------------------- 
if (i && j) { i++; j++; }   

等价于

if (i != 0 && j != 0)  
{
    i++;
    j++;
}                           

等价于

if (i != 0 && j != 0) {   /* K&R C coding style */
        i++;
        j++;
} else {
        ...
}

code example
c
/*
 * K&R C coding style
 */

#include <stdio.h>

int main()
{
        int 	i, j;
    
        printf("input integer i and j: ");
        scanf("%d %d", &i, &j);
    
        if (i > 0 && j > 0) {   /* not equal to (i && j) */
                i--;
                j--;
        } else {
                i++;
                j++;
        }
        printf("i = %d\tj = %d\n", i, j);
        return 0;
}

3.2 条件表达式

tenary operator ?:
三元运算符形成条件表达式语句等价于 if-else 语句:
expr1 ? expr2 : expr3

  • 先计算表达式 expr1
  • 如果 expr1 != 0, 则计算 expr2,并将该值作为三元表达式的值
  • 如果 expr1 == 0, 则计算 expr3,并将该值作为三元表达式的值
  • expr2expr3 只有一个被计算

例如:

c
max = (x > y)? x : y;

if (x > y)
        max = x;
else
        max = y;

3.3 else-if 语句

多路判定语句,语法格式

c
if (expr1)
        statement1
else if (expr2)
        statement2
else if (expr3)
        statement3
else
        statement4
  • 依次测试 expr, 如果某个 expr 为真,则执行相关联的 statement,后续 else if 子句不再执行
  • 最后的 else 子句可选,意为在前面的多路判定都为假时执行
  • statement 既可以是以 ; 结尾的单条语句,也可以是 {} 复合语句块

code example
c
#include <stdio.h>

int main()
{
        int marks, grade;

        printf("Enter your marks: ");
        scanf("%d", &marks);
        if (marks >= 85 && marks <= 100)
                grade = 'A';
        else if (marks >= 60 && marks <85)
                grade = 'B';
        else if (marks >= 40 && marks <60)
                grade = 'C';
        else if (marks >= 0 && marks < 40)
                grade = 'D';
        else {
                printf("marks input error!!!\n");
                printf("marks = %d\n", marks);
                return 0;
        }
        printf("Scored Grade: %c\n", grade);
        return 0;
}

3.3 循环语句 while for

while 循环语法格式:

c
while (expr)
        statement
/* rest C code */
...

循环逻辑:
首先计算 expr 的值。如果其值非 0,则执行 statement,并再次计算 expr 的值。这一循环过程 一直进行下去,直到 expr 的值为 0 为止,随后继续执行 rest C code 注释后面的部分。


code example
c
#include <stdio.h>

int main()
{
        int i;
        
        i = 5;
        while (i > 0) {
                printf("%d\n", i);
                i--;
        }
        printf("loop over!\n");
        return 0;
}

for 循环语法格式:

c
for (expr1; expr2; expr3)
        statement
/* rest C code */
...

循环逻辑:

  • expr1 进行初始化(initialize),只执行一次
  • expr2 进行条件判断(condition)
    如果为真,则执行 statement->expr3->expr2,进行下一次循环
    如果为假,退出循环,继续执行 rest C code 注释后面的部分

code example
c
#include <stdio.h>

int main()
{
        int i;
        
        for (i = 5; i > 0; i--) 
                printf("%d\n", i);
        printf("loop over!\n");
        return 0;
}

for 循环等价于 while 循环

c
expr1;
while (expr2) {
        statement
        expr3;
}
/* rest C code */
...

4 编程示例

exit 函数

  • 头文件: #include <stdlib.h>
  • 函数原型: void exit(int status);
  • 导致正常进程终止,并且 status 返回调用者

4.1 自然数之和

the Sum of Natural Numbers


code example
c
/*
 * the Sum of Natural Numbers
 */

#include <stdio.h>
#include <stdlib.h>

int main()
{
        int n, i, sum = 0;
        
        printf("enter a positive integer: ");
        scanf("%d", &n);
        if (n <= 0) {
                printf("input error, n must greater than 0\n");
                exit(1);
        }
        for (i = 1; i <= n; i++)
                sum += i;
        printf("sum = %d\n", sum);
        return 0;
}
  • 空间分析:
  • 时间分析:

for 循环执行次数

ni = 1i <= nloopi++total
112115
213228
3143311
..................
n1n+1nn3n+2

4.2 整数反转

reverse integer

  • 123   ->   321
  • -25   ->   -52
  • 0   ->   0

code example
c
/*
 * reverse integer
 */

#include <stdio.h>

int main()
{
        int n, rem, rev = 0;    /* rev: reversed, rem: remainder */
        
        printf("Enter a integer: ");
        scanf("%d", &n);
        while (n != 0) {
                rem = n % 10;
                rev = 10 * rev + rem;
                n /= 10;
        }
        printf("Reversed number = %d\n", rev);
        return 0;
}
  • 空间分析:
  • 时间分析:

while 循环分析

nk's loop
1
2
3
......
k
-

计算循环次数 k:


4.3 整数回文

palindrome of integer
回文数:当其数字反转时保持不变。16461, 33, 262, 3553.


code example
c
/*
 * palindrome of integer
 */

#include <stdio.h>

int main()
{
        int n, orig, rem, rev = 0;      /* original, remainder, reversed */
        
        printf("Enter an integer: ");
        scanf("%d", &n);
        orig = n;
        while (n != 0) {
                rem = n % 10;
                rev = rev * 10 + rem;
                n /= 10;
        }
        printf("%d is %s palindrome.\n", orig, (rev==orig)? "a" : "not a");
        return 0;
}
  • 空间分析:
  • 时间分析:

4.4 完美数

perfect number
完美数:所有真因子(除了自身以外的约数)的和,恰好等于它本身。

  • 6 = 1 + 2 + 3
  • 28 = 1 + 2 + 4 + 7 + 14
  • 496 = ...
  • 8128 = ...

code example
c
/*
 * perfect number
 */

#include <stdio.h>

int main()
{
        int num, rem, sum = 0;
        
        printf("Enter an integer: ");
        scanf("%d", &num);
        
        for (int i = 1; i < num; i++) {
                rem = num % i;
                if (rem == 0)
                        sum += i;
        }
        if (sum == num)
                printf("%d is a Perfect Number.\n", num);
        else
                printf("%d is not a Perfect Number.\n", num);
        return 0;
}
  • 空间分析:
  • 时间分析:

4.5 打印星号

星号图案:
*
* *
* * *
* * * *


code example
c
/*
 * print stars
 */

#include <stdio.h>

int main() 
{
        int i, j, rows;
   
        printf("Enter the number of rows: ");
        scanf("%d", &rows);
        for (i = 1; i <= rows; ++i) {
                for (j = 1; j <= i; ++j) 
                        printf("* ");
          		printf("\n");
        }
        return 0;
}
  • 空间分析:
  • 时间分析:

i 值与对应的 j loop 循环次数:

ij loop
11
22
33
......
nn

j loop 循环次数 k:

5 函数

观察函数:printf, scanf, exitmain

  • printf: 函数声明和调用
    • declaration: int printf(const char *format, ...); /* stdio.h */
    • call: printf("hello world\n");
  • scanf: 函数声明和调用
    • declaration: int scanf(const char *format, ...); /* stdio.h */
    • call: scanf("%d", &n);
  • exit: 函数声明和调用
    • declaration: void exit(int status); /* stdlib.h */
    • call: exit(1);
  • main: 函数声明和定义
c
int main()        /* function declaration */
{         /* function definition */
        ...
        return 0;
}

5.1 函数概念

执行特定任务或计算的代码块 {}
接受一组输入,执行特定计算并产生输出(返回值)。
函数包含三个方面:

  • 函数声明: 函数原型, 告知编译器函数名称、函数参数列表和返回类型。
  • 函数定义: 函数体 (由 {} 界定) 包含要执行的实际语句。
  • 函数调用: 可以从程序中的任何位置调用函数。必须传递与函数声明一致的参数。

函数定义的一般形式:

c
return-type function-name(argument declarations)
{
        declarations

        statements
        return expr;
}

函数示例:


两数之和
c
#include <stdio.h>

int sum(int, int);    /* function declaration */

int main()
{
        int x, y, z;

        x = 3;
        y = 8;
        z = sum(x, y);
        printf("%d\n", z);
        return 0;
}

int sum(int a, int b)     /* function declaration and definition */
{
        int sum;

        sum = a + b;
        return sum;
}

两数最大值
c
#include <stdio.h>

int max(int x, int y)
{
        return (x > y)? x : y;
}

int main()
{
        int a, b, c;

        a = 8;
        b = 10;
        c = max(a, b);
        printf("%d\n", c);
        return 0;
}

5.2 局部变量和全局变量

C 程序由全局变量和函数组成。

  • local variable: 局部变量,在函数体内部或块内声明/定义的变量称为局部变量,局部变量由编译器在栈 (stack) 中自动分配内存,因此也叫做自动变量。
  • global variable: 全局变量,在函数体之外声明的变量称为全局变量。全局变量对于其后定义的所有函数均可见。

全局变量
c
#include <stdio.h>

void f();

int 	  x = 10;
int 	  y;

int main()
{
    	  int 	x;

    	  x = 20;
    	  y = 5;
    	  printf("main():\tx = %d\ty = %d\n\n", x, y);    
    	  f();
    	  return 0;
}

void f()
{
    	  printf("f():\tx = %d\ty = %d\n", x, y);
}

5.3 递归函数 - recursion

调用自身的函数称为递归函数。而且,这种技术被称为递归。


自然数之和 - 递归算法
c
#include <stdio.h>

int sum(int n)
{
        if (n == 0)
                return 0;
        return sum(n-1) + n;
}

int main()
{
        int n = 3;

        printf("sum = %d\n", sum(n));
        return 0;
}

正序和倒序打印 n 个自然数
c
#include <stdio.h>

void prtd(int n)
{
        for (int i = 1; i <= n; i++)
                printf("%d\t", i);
        printf("\n");
}

void rprtd(int n)
{
        if (n == 0)
                return;
        rprtd(n-1);
        printf("%d\t", n);
}

int main()
{
        int n = 3;

        prtd(n);
        rprtd(n);
        printf("\n");
        return 0;
}

6 指针与数组 - pointer and array


6.1 指针 - pointer

指针是一种保存变量地址的变量。指针本身的数据类型是 (unsigned long),声明指针需要表明指针指向的变量类型。

打印变量地址:

c
#include <stdio.h> 

int main()
{
    	  int x;
  
    	  printf("%p", &x); 
    	  return 0;
}

指针声明与赋值:

  • int *p, x = 8;p is a pointer to integer
  • p = &x;   指针赋值; 在表达式中 *px 的使用效果相同
    • x = x + 1; <=> *p = *p + 1; (p 指向的对象 +1)
    • x++; <=> (*p)++;
    • ++x; <=> ++*p;

特殊指针类型:

  • void *: 万能指针,可以同其它指针类型进行自由转换
  • NULL: (void *) 0, 空指针

指针本身也是变量,可以进行赋值:

c
int *p, *q;
p = &x;
q = &y;

p = NULL;   /* p 的值为 0, 空指针 */
p = q;      /* p 指向 y */

一个简单例子
c
#include <stdio.h>
int main()
{ 
    	  int x = 10;
    	  int *ptr;

    	  ptr = &x;
    	  printf("%d\n", *ptr);
	      *ptr = 20;
	      printf("%d\n", x);
    	  return 0;
}

整数交换版本1
c
#include <stdio.h>

void swap(int x, int y)
{
        int temp;

        temp = x;
        x = y;
        y = temp;
}

int main()
{
    	  int a, b;

    	  a = 10;
    	  b = 20;
    	  swap(a, b);
    	  printf("a = %d\tb = %d\n", a, b);
    	  return 0;
}

整数交换版本2
c
#include <stdio.h>

void swap(int *px, int *py)
{
        int temp;

        temp = *px;
        *px = *py;
        *py = temp;
}

int main()
{
    	  int a, b;

    	  a = 10;
    	  b = 20;
    	  swap(&a, &b);
    	  printf("a = %d\tb = %d\n", a, b);
    	  return 0;
}

6.2 数组 - array

数组是存储在连续内存位置的相同数据类型的数据项的集合,可以使用数组的索引随机访问元素。

6.2.1 数组声明和初始化

可以通过多种方式声明数组。可以通过指定它的类型和大小,通过初始化它或两者来完成。

声明和初始化

c
int arr[10];

int n = 10;
int arr[n];

int arr[] = { 10, 20, 30, 40 };		  /* initialization */

int arr[4] = {10, 20, 30, 40};		  /* initialization */

int arr[6] = { 10, 20, 30, 40 };		/* {10, 20, 30, 40, 0, 0} */

WARNING

数组不能直接赋值,下面的语句是非法的:
int a[3];
a = {2, 10, 7};   /* compile error!!! */

6.2.2 数组访问

  • 使用整数索引访问数组元素。
  • 数组索引从 0 开始,一直到数组大小减 1。
  • 数组元素与变量等同。
code example
c
#include <stdio.h>
 
int main()
{
      	int 	arr[5], x, y = 18;

      	arr[0] = 5;
      	arr[2] = -10;
      	arr[3 / 2] = 2; 	/* same as arr[1] = 2 */
      	arr[3] = arr[0];
  
   	    x = arr[3];
  	    arr[4] = y;

      	printf("%d %d %d %d", arr[0], arr[1], arr[2], arr[3]); 
      	return 0;
}

数组索引没有越界检查。

code example
c
#include <stdio.h>
 
int main()
{
        int arr[2];

        arr[0] = 12;
        arr[1] = 5;
        printf("%d ", arr[3]);
        printf("%d ", arr[-2]); 
        return 0;
}

数组具有地址和大小两个属性:

  • 地址为首元素(索引0)地址
  • 大小为数组所有元素大小之和(sizeof运算符,编译时属性)
code example
c
#include <stdio.h>
int main()
{
    	  int arr[3], i;
  
    	  printf("int size: %lu\n\n", sizeof(int));
    	  printf("size of arr: %lu\n", sizeof(arr));
    	  printf("number of elems: %lu\n", sizeof arr / sizeof arr[0]);
    	  printf("arr address: %p\n\n", arr);
    	  for (i = 0; i < 5; i++)				/* overflow */
          		  printf("address of arr[%d] is %p\n", i, &arr[i]); 
    	  return 0;
}

数组的遍历 - 打印数组元素
c
#include <stdio.h>

int main() 
{
    	  int n, arr[] = {1, 2, 3, 4};

    	  n = sizeof(arr) / sizeof(arr[0]);
    	  for (int i = 0; i < n; i++)
          		  printf("%d\t", arr[i]);
        printf("\n");
    	  return 0;
}

6.3 指针与数组

在 C 语言中,指针和数组之间的关系十分密切,指针与数组适合放在一起讨论。

6.3.1 等价关系

在表达式中,数组名称自动转换为指向数组首地址的指针:

c
int a[5], *p, x;
p = a;

数组名称在编译时转换为指针:

p = a; <=> p = &a[0];
a[0]; <=> p[0]; <=> *p; <=> *a; <=> *(p + 0); <=> *(a+0);
a[i]; <=> p[i]; <=> *(p + i);
x = a[i]; <=> x = p[i]; <=> x = *(p + i);
p = &a[i]; <=> p = a + i;

WARNING

指针是变量,而数组名不是变量,类似下列语句是非法的:
a = p;
a++;

6.3.2 指针算术运算

如果 p 是指向某种数据类型的指针,i 是整数类型,则有:

  • p+1: 指向 p 所指向的对象的下一个对象
  • p+i: 指向 p 所指向的对象之后的第 i 个对象
  • p++,++p: p 指向其所指对象的下一个对象
  • p--,--p: p 指向其所指对象的前一个对象

如果 p q 是指向相同数据类型的指针,offset 是整数类型表示偏移量,则有:

  • q = p + offset;
  • offset = q - p;
  • p + q: 编译器错误,两个地址相加没有意义,会造成地址空间溢出
  • if (p == q): 测试指针是否相等

对于 NULL 指针:

  • p = NULL: 确保指针 p 不可用,避免指向不存在的对象
  • if (p == NULL): 测试指针 p 是否可用

6.4 编程练习


printarray - 打印数组
c
#include <stdio.h>

void printarray(int a[], int n)       /* a[] 等同于 *a */
{
        int i;
        
        for (i = 0; i < n; i++) {
                printf("%3d", a[i]);
                if (i%5 == 4 || i == n-1)
                        printf("\n");
                else
                        printf(" ");
    	}
}
int main() 
{
        int arr[] = {1, 2, 3, 4, 5, 6};
        int n = sizeof(arr) / sizeof arr[0];
        
        printarray(arr, n);
        return 0;
}

findelem - 查找数组元素
c
#include <stdio.h>

int findelem(int key, int a[], int n)
{
        for (int i = 0; i < n; i++)
                if (a[i] == key)
                        return i;
        return -1;      /* not found */
}

int main()
{
        int idx, arr[5] = { 10, 30, 15, 40, 20 };
            
        idx = findelem(15, arr, 5);
        if (idx != -1)
                printf("key 15 found at index: %d\n", idx);
        else
                printf("key 15 does not found\n");
        return 0;
}

maxofa - 数组的最大值
c
#include <stdio.h>

int maxofa(int a[], int n)
{
        int i, max;

        for (max = a[0], i = 0; i < n; i++)
                if (a[i] > max)
                        max = a[i];
        return max;
}
int main()
{
        int arr[] = {12, 2, 8, 7, 24, 11};
        int n = sizeof(arr) / sizeof(arr[0]);
        
        printf("max of array is: %d\n", maxofa(arr, n));
        return 0;
}

average - 数组的平均值
c
#include <stdio.h>

double average(int a[], int n)
{
        int i;
        double sum;

        for (sum = 0.0, i = 0; i < n; i++)
                sum += a[i];
        return sum/n;
}
int main() 
{
        int arr[] = {88, 76, 92, 83, 80, 95};
    	  int n = sizeof(arr) / sizeof(arr[0]);

        printf("average of array is: %.2f\n", average(arr, n));
        return 0;
}

7 常量,字符串,字符数组


7.1 数字常量,字符常量

整形常量:

数据类型例1例2
dec23150
octal0370177
hex0x1f0X7F
unsigned23U150u
long23L150l
unsigned long23UL150ul

浮点常量:

数据类型例1例2例3
double2.3812.51e-2
float2.38f12.5f1e-2f

字符常量:
一个字符常量是一个整数,书写时将一个字符括在单引号中,如,'x'。字符在机器字符集中的数值就是字符常量的值。

字符常量
'0' ~ '9'48 ~ 57
'A' ~ 'Z'65 ~ 90
'a' ~ 'z'97 ~ 122
'\0'0
'\t'9
'\n'10
' '32

字符常量 '\0' 表示值为 0 的字符,也就是空字符(null)。我们通常用 '\0' 的形式代替 0,以强调某些表达式的字符属性,但其数字值为 0

字符常量一般用来与其它字符进行比较,但也可以像其它整数一样参与数值运算:

c
int i;
char c;
i = '8' - '0';
c = 'a' + i;

7.2 字符数组与字符串

数据类型为 char 的数组,或者说字节 (bytes) 数组。


字符数组
c
#include <stdio.h>

int main() 
{
        char name[7] = {'R', 'i', 't', 'c', 'h', 'i', 'e'};

        for (int i = 0; i < 7; i++)
                printf("%c", name[i]);
        printf("\n");
        return 0;
}

字符数组的使用规则同其它数据类型的数组相同。

当一个字符数组以 '\0' (0) 结尾时,这样的字符数组称为字符串。


字符串
c
#include <stdio.h>

int main()
{
        char name[] = { 'R', 'i', 't', 'c', 'h', 'i', 'e', '\0' };

        for (int i = 0; name[i] != '\0'; i++)
                printf("%c", name[i]);
        printf("\n\n");
        
        printf("%s\n", name);
        return 0;
}

字符数组与字符串的区别:

  • 字符数组通常用来声明一个字节缓冲区,数据类型由程序员根据需要设定和转换。
  • 字符串通常用来表示连续存放的 char 类型,通过结尾 0 对连续字符进行打包。因此字符串占用的内存空间比字符串本身的长度多一位以容纳 '\0'
  • 当操作指向字符数组的指针时我们关心的是数组的长度以避免指针越界;对于指向字符串的指针我们通常关系的是结尾 '\0' 以指示是否到达字符串的末尾。
  • 终止 '\0' 隐含了字符串的长度信息。

同数组一样,字符数组或字符串不能进行整体赋值操作。

字符串数组有更简单的声明方式:

char name[] = "Dennis Ritchie";

" " 包围的字符串称为 字符串字面值 - string literal。


计算字符串长度
c
#include <stdio.h>

int main()
{
        char	name[] = "Brian Kernighan";
        int 	len;
        
        len = 0;
        while (name[len] != '\0')
                len++;
        printf("name: %s\n", name);
        printf("string length: %d\n", len);
        printf("name size: %lu", sizeof name);
        return 0;
}

C 语言提供了许多字符串操作的常规库函数, 上面的字符串长度可通过库函数计算:

c
#include <string.h>
int l = strlen(name);

几个常用字符串函数 (include <string.h>):

  • char *strcpy(s, ct): 将字符串 ct(包括 '\0')复制到字符串 s 中,并返回 s
  • char *strcat(s, ct): 将字符串 ct 连接到 s 的尾部,并返回 s
  • int strcmp(cs,ct): 比较字符串 csct;当 cs<ct 时,返回一个负数;当 cs==ct 时,返回 0;当 cs>ct 时,返回一个正数
  • size_t strlen(cs): 返回字符串 cs 的长度

字符串常量 - string literal

字符串常量是一个字符数组,例如:
"I am a student"
对于函数参数,如
printf("hello, world\n");
当类似于这样的一个字符串出现在程序中时,实际上是通过字符指针访问该字符串的。在上述语句中,printf 接受的是一个指向字符数组第一个字符的指针。
也就是说,字符串常量可通过一个指向其第一个元素的指针访问。

char *s;
s = "my test";

8 动态内存分配 - malloc and free

动态内存分配,在运行时分配内存。

void * 通用指针

  • 不与任何数据类型关联的指针
  • 指向存储中的某个数据位置,即指向变量的地址
  • 可以同其它类型指针进行自由转换

指针类型转换
c
#include <stdio.h>
#include<stdlib.h>

int main() {
        int	i = 5;
        float	f = 8.3;
        void 	*p;
        int  	*ip;
        float *fp;
        
        p = &i;
        ip = (int *) p;
        printf("Integer variable is = %d\n", *((int*) p));
        printf("*ip value: %d\n\n", *ip);

        p = &f;
        fp = (float *) p;
        printf("Float variable is = %f\n", *((float*) p));
        printf("*fp value: %f\n", *fp);
        return 0;
}

malloc, free 动态内存分配和释放函数:

c
#include <stdlib.h>

void *malloc(size_t size);
void free(void *ptr);
  • malloc 分配 size 字节并返回一个指针到分配的内存。 内存未初始化。
  • free 释放 ptr 指向的内存空间 (由 malloc 分配)
c
int     *p;

p = (int *) malloc(sizeof(int));
if (p == NULL) {
        fprintf(stderr, "malloc error");
        exit(1);
}



free(p);
  • fprintf: 格式化打印输出函数,printf 的完全版
  • stderr: 标准错误输出

字符串复制
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char *sdup(char *s)
{
    	  char    *t;
    	  int     len;

    	  len = strlen(s);
    	  if ((t = malloc(len + 1)) == NULL) {
                fprintf(stderr, "malloc error");
                exit(1);
    	  }
    	  for (int i = 0; i < len; i++)
                t[i] = s[i];
    	  t[len] = '\0';
    	  return t;
}

int main()
{
    	  char    *str;

    	  str = sdup("Hellow, world");
    	  printf("%s\n", str);
    	  free(str);
    	  return 0;
}

C 语言提供了库函数 strdup

c
#include <string.h>
char *strdup(const char *s);

strdup() 函数返回一个指向新字符串的指针,该字符串是字符串 s 的副本。 新字符串的内存使用 malloc 获得,并且可以使用 free 释放。

9 结构和联合


9.1 结构 - struct

结构是 C 中一个用户定义的数据类型,它允许组合不同类型的数据项。

结构类型声明:

c
struct point {
	int	x;
	int	y;
};

结构类型要点:

  • 复杂数据类型在内存中的布局描述
  • 结构类型变量的声明和定义:struct point a, *pa;
  • 结构变量通过 . 运算符访问结构成员:a.x
  • 结构指针通过 -> 运算符访问结构成员:pa->y
  • 结构通过 {}进行声明初始化
  • 结构可以进行赋值,也可以作为函数参数传递

定义并访问结构
c
#include <stdio.h>

struct book {
        char    *title;
        char    *author;
        int     id;
};

int main()
{
	struct book book = {}, book1;
	
	book1.title = "The C programming language";
	book1.author = "Brain W. Kernighan & Dennis M. Ritchie";
	book1.id = 12806;
	
	struct book book2 = {"The UNIX programming environment",
				"Kernighan & Rob Pike", 937699};
	
	book = book1;	/* struct assignment, memory copy */
	printf("title: %s\n", book.title);
	printf("author: %s\n", book.author);
	printf("book id: %d\n\n", book.id);
	
	book = book2;
	printf("title: %s\n", book.title);
	printf("author: %s\n", book.author);
	printf("book id: %d\n", book.id);
	return 0;
}

变量与指针,参数传递
c
#include <stdio.h>

struct book {
        char    *title;
        char    *author;
        int     id;
};

void print1(struct book book)
{
        printf("title: %s\n", book.title);
        printf("author: %s\n", book.author);
        printf("book id: %d\n", book.id);         
}

void print2(struct book *b)
{
        printf("title: %s\n", b->title);
        printf("author: %s\n", b->author);
        printf("book id: %d\n", b->id);         
}

int main()
{
        struct book book = {}, book1;
        
        book1.title = "The C programming language";
        book1.author = "Brain W. Kernighan & Dennis M. Ritchie";
        book1.id = 12806;
        
        struct book book2 = {"The UNIX programming environment",
                                "Kernighan & Rob Pike", 937699};
        
        book = book1;   /* struct assignment, memory copy */
        print1(book);
        printf("\n");
        book = book2;
        print2(&book);
        return 0;
}

9.2 typedef

用于为一种数据类型创建附加名称(别名),但不创建新类型,通常用于简化声明复杂的类型组成的结构。


动态内存分配
c
#include <stdio.h>
#include <stdlib.h>

typedef struct tag_point point;
struct tag_point{
	int     x;
	int	y;
};

typedef struct tag_book{
	char    *title;
	char	*author;
	int	id;
}book;

int main()
{
        point	p = {3, 5};
        book    *b;
        
        if ((b = malloc(sizeof(*b))) == NULL) {
                fprintf(stderr, "malloc error\n");
                exit(1);
        }
        b->title = "tcpl";
        b->author = "bwk & dmr";
        b->id = 12598;
        
        printf("point:\nx = %d\ny = %d\n\n", p.x, p.y);
        printf("book:\n");
        printf("title: %s\nauthor: %s\nid: %d\n",b->title, b->author, b->id);
        return 0;
}

9.3 联合 - union

union 允许在同一内存位置存储不同的数据类型。可以定义具有许多成员的联合,但在任何给定时间只有一个成员可以包含值。联合提供了一种将同一内存位置用于多种用途的有效方式。

union 的语法同 struct 相同。

switch - case 语句

switch 语句是一种多路判定语句,它测试表达式是否与一些常量整数值中的某一个值匹配,并执行相应的分支动作。

c
switch(expr) {
case const-expr: 
        statements
        break;
case const-expr: 
        statements
        break;
default:
        statements
}
  • break 语句将导致程序的执行立即从 switch 语句中退出
  • 当所有 case 语句都不匹配时执行 default 语句,并从 switch 语句中退出
  • 在 switch 语句中,case 的作用只是一个标号

union 示例
c
#include <stdio.h>
#include <stdlib.h>

typedef union {
    	int ival;
    	float fval;
}datum;

datum add(datum d1, datum d2, int flag)
{
        datum   d;
    
        switch(flag) {  /* 0 integer val, 1 float val */
        case 0:
                d.ival = d1.ival + d2.ival;
                break;
        case 1:
                d.fval = d1.fval + d2.fval;
                break;
        default:
                fprintf(stderr, "add: unsupport flag: %d\n", flag);
                exit(1);
        }
        return d;
}

int main()
{
        datum   d1, d2, d3;

        d1.ival = 3;
        d2.ival = 10;
        d3 = add(d1, d2, 0);
        printf("3 + 10 = %d\n", d3.ival);

        d1.fval = 2.5;
        d2.fval = 3.1;
        d3 = add(d1, d2, 1);
        printf("2.5 + 3.1 = %.2f\n", d3.fval);
        return 0;
}

10.1 自引用结构 - self referential structure

具有一个或多个指针成员,这些成员指针指向结构本身类型。

c
struct node {
        int     data;
        struct node *next;
};

需要考虑的重要一点是指针应该在访问之前正确初始化,因为默认情况下它包含垃圾值。


newnode - 创建新节点
c
#include <stdio.h>
#include <stdlib.h>

struct node {
        int     data;
        struct node *next;
};

struct node *newnode(int x)
{
        struct node *n;

        if ((n = malloc(sizeof(*n))) == NULL) {
                fprintf(stderr, "newnode: malloc error\n");
                exit(1);
        }
        n->data = x;
        n->next = NULL;
        return n;
}
int main()
{
        struct node *p;

        p = newnode(10);
        p->next = newnode(20);
        printf("node data: %d\n", p->data);
        printf("next node data: %d\n", p->next->data);
        return 0;
}

WARNING

为了简化问题,在本示例中,我们最后没有释放 (free) malloc 分配的 node 节点内存。
当程序退出时,malloc 分配的内存会被释放。


链表 (link list) 是包含一系列链接节点 (node)的线性数据结构。每个节点 (node) 存储数据和下一个节点的地址。

每个节点都是相同的自引用结构,包括:

  • 一个数据项 (data item)
  • 下一个节点的地址 (pointer to another node, or NULL)

printlink - 打印链表
c
#include <stdio.h>
#include <stdlib.h>

struct node {
        int data;
        struct node *next;
};

struct node *newnode(int);
void printlink(struct node *);

int main()
{
        struct node *head, *one, *two, *three;

        one = newnode(10);
        two = newnode(20);
        three = newnode(30);

        one->next = two;
        two->next = three;
        head = one;
        printlink(head);
        return 0;
}

void printlink(struct node *p)
{
        while (p != NULL) {
                printf("%3d ", p->data);
                p = p->next;
        }
        printf("\n");
}

struct node *newnode(int x)
{
        struct node *n;

        if ((n = malloc(sizeof(*n))) == NULL) {
                fprintf(stderr, "newnode: malloc error\n");
                exit(1);
        }
        n->data = x;
        n->next = NULL;
        return n;
}

atolink - 数组转换为链表
c
#include <stdio.h>
#include <stdlib.h>

struct node {
        int data;
        struct node *next;
};

struct node *newnode(int);
void printlink(struct node *);
struct node *atolink(int *, int);

int main()
{
        int arr[] = { 10, 20, 30, 40, 50 };
        struct node *head;
    
        head = atolink(arr, sizeof(arr)/sizeof(arr[0]));
        printlink(head);
        return 0;
}

struct node *atolink(int a[], int n)    /* convert array to link list */
{
        struct node head = {}, *cur;
    
        cur = &head;
        for (int i = 0; i < n; i++)
                cur = cur->next = newnode(a[i]);
        return head.next;
}

void printlink(struct node *p)
{
        while (p != NULL) {
                printf("%3d ", p->data);
                p = p->next;
        }
        printf("\n");
}

struct node *newnode(int x)
{
        struct node *n;

        if ((n = malloc(sizeof(*n))) == NULL) {
                fprintf(stderr, "newnode: malloc error\n");
                exit(1);
        }
        n->data = x;
        n->next = NULL;
        return n;
}

11 排序和查找


11.1 冒泡排序 - bubblesort


bubble - 冒泡排序
c
#include <stdio.h>

void swap(int *, int *);
void printarray(int *, int);

void bubble(int v[], int n)
{
        int i, j;

        for (i = 0; i < n; i++)
                for (j = 0; j < n-i-1; j++)
                        if (v[j] > v[j+1])
                                swap(&v[j], &v[j+1]);
}

int main()
{
        int arr[] = {64, 34, 25, 12, 22, 11, 90};
        int n = sizeof(arr)/sizeof(arr[0]);

        bubble(arr, n);
        printf("Sorted array: \n");
        printarray(arr, n);
        return 0;
}

void swap(int *x, int *y)
{
        int t = *x;
        *x = *y;
        *y = t;
}

void printarray(int arr[], int n)
{
        int i;

        for (i=0; i < n; i++)
                printf("%d ", arr[i]);
        printf("\n");
}


binsearch - 二分查找
c
#include <stdio.h>

void swap(int *, int *);
void printarray(int *, int);
void bubble(int *, int);

int binsearch(int x, int v[], int n)
{
        int low, high, mid;

        low = 0;
        high = n - 1;
        while (low <= high) {
                mid = (low + high) / 2;
                if (x < v[mid])
                        high = mid - 1;
                else if (x > v[mid])
                        low = mid + 1;
                else    /* found match */
                        return mid;
    	}
    	return -1;      /* no match */
}

int main()
{
        int arr[] = {64, 34, 25, 12, 22, 11, 90};
        int n = sizeof(arr)/sizeof(arr[0]);

        bubble(arr, n);
        printf("Sorted array: \n");
        printarray(arr, n);
        printf("Index of 25 is: %d\n", binsearch(25, arr, n));
        printf("Index of 8 is: %d\n", binsearch(8, arr, n));
        return 0;
}

12 预处理器 - preprocessor

预处理器执行宏替换、条件编译以及包含指定的文件,以 # 开头的命令行就是预处理器处理的对象。

预处理器有单独的语法,它不是 C 语言的一部分。

  • #include 指令

    包含预定义的头文件名,并以 < > 包围, 如:
    #include <stdio.h>
    #include <stdlib.h>

  • #define 指令

    #define 指令允许在源代码中定义宏。这些宏定义允许声明常量值以在整个代码中使用。
    宏定义不是变量,不能像变量一样被程序代码更改。在创建表示数字、字符串或表达式的常量时,通常会使用此语法。


define 宏定义
c
#include <stdio.h>

#define NAME    "Tom"
#define AGE     10
#define PI      3.14

int main()
{
        printf("%s is over %d years old.\n", NAME, AGE);
        printf("PI's value: %f\n", PI);
        return 0;
}
  • 参数宏

    类函数宏可以接受参数,就像真正的函数一样。参数必须是有效的 C 标识符,以逗号和可选的空格分隔。


参数宏
c
#include <stdio.h>

#define MAX(a,b) (a) > (b) ? (a) : (b)

int main()
{
        int x, y, z;

        x = 10;
        y = 20;
        z = MAX(x, y);  /* (x) > (y) ? (x) : (y) */
        printf("max of x and y: %d\n", z);
        return 0;
}

WARNING

宏参数 a, b 可以是表达式,为了保证正确的优先级,将 a, b 分别用 () 括起来是必要的。

  • 跨行连接

    预处理器单行解析,行尾不需要 ; 终结,如果一条指令需要跨越多行,需要在行尾添加 \ 进行连接:

c
#define MACROS	long line ...   \
                continue line   \
                end line

13 课程回顾

C 语言关键字:

123456
autobreakcasecharconstcontinue
defaultdodoubleelseenumextern
floatforgotoifintlong
registerreturnshortsignedsizeofstatic
structswitchtypedefunionunsignedvoid
volatilewhile

C 语言库函数:

  • int printf(const char *format, ...);
  • int fprintf(FILE *stream, const char *format, ...);
  • int scanf(const char *format, ...);
  • void exit(int status);
  • char *strcpy(char *dest, const char *src);
  • char *strcat(char *dest, const char *src);
  • int strcmp(const char *s1, const char *s2);
  • size_t strlen(const char *s);
  • void *malloc(size_t size);
  • void free(void *ptr);

一些关注点:

  • `define NULL (void *)0
  • typedef long unsigned int size_t
  • '\0' 空字符 0
  • "" 空字符串,只包含字符 '\0'
  • void 空类型,作为函数无返回值类型,或者函数参数为空
  • void * 通用指针类型
  • stdin, stdout, stderr 预定义的文件指针 (File *) 类型,标准输入,标准输出和标准错误

下一步是什么?

数据结构和算法,C语言实现

14 附录: ACSII编码表和运算符优先级表


ASCII CODE TABLE
DecCharDecCharDecCharDecChar
0NUL32SP64@96`
1SOH33!65A97a
2STX34"66B98b
3ETX35#67C99c
4EOT36$68D100d
5ENQ37%69E101e
6ACK38&70F102f
7BEL39'71G103g
8BS40(72H104h
9TAB41)73I105i
10LF42*74J106j
11VT43+75K107k
12FF44,76L108l
13CR45-77M109m
14SO46.78N110n
15SI47/79O111o
16DLE48080P112p
17DC149181Q113q
18DC250282R114r
19DC351383S115s
20DC452484T116t
21NAK53585U117u
22SYN54686V118v
23ETB55787W119w
24CAN56888X120x
25EM57989Y121y
26SUB58:90Z122z
27ESC59;91[123{
28FS60<92\124|
29GS61=93]125}
30RS62>94^126~
31US63?95_127DEL

运算符的优先级与结合性
OperatorsAssociativity
() [] -> .left to right
! ~ ++ -- + - * & (type) sizeofright to left
* / %left to right
+ -left to right
<< >>left to right
< <= > >=left to right
== !=left to right
&left to right
^left to right
|left to right
&&left to right
||left to right
?:right to left
= += -= *= /= %= &= ^= |= <<= >>=right to left
,left to right

This website is released under the MIT License.