Back
Featured image of post Linux C(一)入门

Linux C(一)入门

程序的基本概念

程序和编程语言

程序(Program) 是由一系列指令(Instruction)组成,它告诉计算机应如何完成一个计算任务。

这些指令通常会有:输入(从输入设备中获取数据)、输出(在输出设备中输出数据)、基本运算(例如数学上的四则运算)、流程控制(条件分支或者循环其他指令)。

编写程序就是将指令进行组合,来完成复杂任务。

编程语言(Programming Language)分为低级语言(Low-level Language)和高级语言(High-level Language)。

机器语言(Machine Language)和汇编语言(Assembly Language)属于低级语言,直接用计算机指令编写程序。而C、C++、Java、Python等属于高级语言,它们用语句(Statement)来编写程序,语句是计算机指令的抽象表示。

编译器(Compiler)将C语言的语句编译(Compile)成汇编或机器指令,计算机才能执行。

虽然说编译需要花费一些时间,但是这样会使编程变得更加容易。


C语言是 可移植的(Portable) 的,为什么这么说呢。

因为不同的计算机体系是有不同的指令集(Instruction Set)的,那么能识别到机器指令格式也不同。如果不同机器上都有C语言的编译器,那么我们就可以将C语言编译成不同机器都能识别的机器指令。

那么,程序执行的流程就是:源码通过编译生成可执行文件,然后在操作系统上加载运行这个可执行程序。

当然,如果是脚本语言,比如:Shell、Python等等,则省去编译的这个过程。

程序的调试

程序的编写通常没有那么容易,程序中错误叫做 BUG,修正 BUG 的过程叫做 调试(Debug) ,程序中的 BUG 通常有这么几类:

  • 编译时错误,例如一些语法错误。
  • 运行时错误,可以编译成可执行文件,但是在执行时程序会崩溃。
  • 逻辑错误和语义错误,编译和执行都很顺利,但是没有按期望去执行。

第一个程序

我们将 C语言程序写在 main.c 的文件里。

# include <stdio.h>

int main(){
    pritf("Hello, World!\n");
    return 0;
}

我们通过 gcc 对 C语言进行编译,它会将我们的 main.c 源代码编译生成可执行文件 a.out ,当然我们也可以指定其生成的文件名。

gcc -Wall main.c -o main
./main		# Hello, World!

一个好的习惯是打开 gcc-Wall 选项,也就是让 gcc 提示所有的警告信息,不管是严重的还是不严重的,然后把这些问题从代码中全部消灭。

常量、变量和表达式

简单的规则

# include <stdio.h>

// 单行注释

/*
	多行注释
*/

int main(void){
    printf("注释不会影响编译!");
    return 0;
}

除了注释,我们用到了由双引号(Double Quote)引起来的一串字符,它称为 字符串字面值(String Literal) 或者字符串。

打印的时候,我们并不会打印双引号,因为它是字符串字面值的界定符号。


# include <stdio.h>


int main(void){
    printf("Hello, world.\n");
	printf("Goodbye, ");
	printf("cruel world!\n");
    return 0;
}

上面的代码中,我们用到了 转义序列(Escape Sequence) ,它们代表着一些特殊的意义。

  • \n:换行符号。
  • \t:制表符号。
  • \b:回退符号。

常量

常量(Constant) 是程序中最基本的元素,有字符(Character)常量、整数(Integer)常量、浮点数(Floating Point)常量和枚举常量。

# include <stdio.h>


int main(void){
	printf("character: %c\ninteger: %d\nfloating point: %f\n", '}', 34, 3.14);
    return 0;
}

这里需要注意的是,计算机中整数和小数点内部表达方式不同,这是因为它们的基本存储方法不同。

其中,%c%f%d 是字符类型的转换说明,这种用法通常叫做占位符(Placeholder),只是在打印输出的时候改变显示的内容,但不是对实际的内容进行修改。

变量

变量(Variable) 是编程语言最重要的概念之一,变量是计算机存储器中的一块命名的空间,可以在里面存储一个值(Value)。

C语言规定必须以字母或下划线_(Underscore)开头,后面可以跟若干个字母、数字、下划线,但不能有其它字符。

# include <stdio.h>

int main(){
    // 变量声明
    char cc;	// 字符型变量
    int bb;		// 整型变量
    float ss;	// 单精度浮点数
    double tom;		// 双精度浮点数
    
    return 0;
}

C语言中的声明(Declaration)有变量声明、函数声明和类型声明三种。如果一个变量或函数的声明要求编译器为它分配存储空间,那么也可以称为定义(Definition),因此定义是声明的一种。

赋值

声明或定义了变量之后,我们要把值存到它们所表示的存储空间里,可以用赋值(Assignment)语句。

变量的定义和赋值也可以一步完成,这称为变量的 初始化(Initialization)

# include <stdio.h>

int main(){
    // 变量赋值
    char firstletter;
    int hour, minute;
    firstletter = 'a';   /* give firstletter the value 'a' */
    hour = 11;           /* assign the value 11 to hour */
    minute = 59;         /* set minute to 59 */
    return 0;
}

注意变量一定要先声明后使用,它们代表着各自的存储空间,编译器必须先看到变量声明,才知道怎么对变量进行读写。

表达式

常量和变量都可以参与加减乘除运算,比如 +-*/ 就代表着四则运算的符号,就叫运算符(Operator),参与运算的变量叫操作数(Operand)比如 a + bab 就是操作数,而 a + b 这一整个式子,就是表达式(Expression)

# include <stdio.h>

int main(){
    int total_minute;
	total_minute = hour * 60 + minute;
}

字符类型和字符编码

# include <stdio.h>

int main(){
	printf("%c\n", 'a'+1);
}

在计算机内,每个符号都有对应的整数来进行表示,我们称为 字符编码(Character Encoding) ,最常用的字符编码规则就是 ASCII码(American Standard Code for Information Interchange,美国信息交换标准码)。

简单函数

数学函数

在数学中我们用过sin和ln这样的函数,例如sin(π/2)=1,ln1=0等等,在C语言中也可以使用这些函数。

# include <math.h>
# include <stdio.h>

int main(void){
    double pi = 3.1416;
	printf("sin(pi/2)=%f\nln1=%f\n", sin(pi/2), log(1.0));
	return 0;
}

我们编译运行这个程序。

gcc main.c
./a.out		# sin(pi/2)=1.000000 	ln1=0.000000

上面的代码中,log 我们叫它 函数(Function) log(1.0) 我们叫函数调用(Function Call),这些函数就放在 math.h 的这个文件内,所以要使用这些函数就要导入这个文件。

其实,我们已经接触过函数了,就是我们的 main 函数,它前面的 int 是指函数运行完毕后返回的值类型是一个整数,return 语句负责函数的返回。

自定义函数

#include <stdio.h>

void newline(void)
{
	printf("\n");
}

void threeline(void)
{
	newline();
	newline();
	newline();
}

int main(void)
{
	printf("Three lines:\n");
	threeline();
	printf("Another three lines.\n");
	threeline();
	return 0;
}

上述代码说明了几个事情,就是函数的调用只需要写它的函数名和圆括号就行了,除非它有要求参数,不然就不用进行参数传递。

此外,如果函数没返回值,那么在定义的时候写上 void,这样函数就不用写 return 语句了。

最后,同一个函数是可以被多次调用的,但是要在调用前进行定义。

形参和实参

#include <stdio.h>

void print_time(int hour, int minute)
{
	printf("%d:%d\n", hour, minute);
}

int main(void)
{
	print_time(23, 59);
	return 0;
}

形参相当于函数中定义的变量,调用函数传递参数的过程相当于定义形参变量并且用实参的值来初始化,如果我们对形参进行修改,结果并不会影响到实际参数。

其实就是相当于将实际参数复制了一份给函数内部进行使用,使用完后,形式参数就会被回收。

全局变量、局部变量

我们把函数中定义的变量称为 局部变量(Local Variable) 。它的含义是,函数中定义的变量不能被其他函数使用,每次调用函数的时候会创建新的局部变量,它们的存储空间是不同的。

全局变量(Global Variable),全局变量定义在所有的函数体之外,它们在程序开始运行时分配存储空间,在程序结束时释放存储空间,在任何函数中都可以访问全局变量。

分支语句

if 语句

# include <stdio.h>

int main(){
    int x = 12;
    if (x != 0) {
		printf("x is nonzero.\n");
	}
}

其中,!= 表示不等于的意思,x != 0这个表达式称为 控制表达式(Controlling Expression),如果条件成立,那么 {} 里面的语句就会执行,否则就不执行。

!= 是关系运算符,此外还有 == 表示相等的关系运算符,自然还有其他的关系运算符,这里就按下不表了。

if/else 语句

# include <stdio.h>

int main(){
    int x = 7;
    // if - else
    if (x % 2 == 0)
		printf("x is even.\n");
	else
		printf("x is odd.\n");
    
    // if - else if - else
    if (x > 0)
        printf("x is positive.\n");
    else if (x < 0)
        printf("x is negative.\n");
    else
        printf("x is zero.\n");
}

通常 if 语句与 else 语句搭配使用,要解释一下的是 % ,它是取模(Modulo)运算符,计算余数用的。

布尔代数

# include <stdio.h>

int main(){
    int a = 1, b = 2, c =3;
    if (a < b && b < c) {
		printf("b is between a and c.\n");
	}
    return 0;
}

其中 && 是逻辑运算符号,叫做逻辑与(Logical AND),仅仅当左右两边的条件成立时才为真。此外,还有逻辑或(Logical OR)||,仅有一个条件成立就为真。逻辑非(Logical NOT)! ,直接取反条件。

swtich 语句

# include <stdio.h>

void print_day(int day){
    switch (day){
        case 1:
        case 2:
        case 3:
        case 4:
        case 5:
            printf("Work Day!");
            break;
        case 6:
        case 7:
            printf("Weekend!");
            break;
        default:
            printf("Illegal Day Number!");
            break;
           
    }
}


int main(){
    printf_day(2);
    return 0;
}

深入理解函数

return 语句

# include <stdio.h>

int absolute_value(int x)
{
	if (x < 0) {
		return -x;
	} else if (x > 0) {
		return x;
	}
}

int main(){
    printf(absolute_value(-10));
    return 0;
}

函数返回一个值相当于定义一个和返回值类型相同的临时变量并用 return 后面的表达式来初始化。

增量式开发

增量式(Incremental) 开发的思路就是将大的问题分成小的问题,然后再让小问题分成更小的问题,这个过程在代码中的体现就是函数的分层设计(Stratify)。

编写一个程序求圆的面积,圆的半径以两个端点的座标(x1, y1)和(x2, y2)给出。

  • 1、根据两点坐标求出圆的半径。
  • 2、根据半径求出圆的面积。
# include <stdio.h>

double distance(double x1, double y1, double x2, double y2){
    return sqrt((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2));
}

double area(double r){
    return r*r*3.1416;
}

int main(){
    printf(are(distance(1.0, 2.0, 4.0, 6.0)));
    return 0;
}

递归

# include <stdio.h>

int factorial(int n){
    if (n == 0){
        return 1;
    }
    return factorial(n-1)*n
}

int Fibonacci(int n){
    if (n == 0 || n == 1){
        return n
    }
    return Fibonacci(n-1) + Fibonacci(n-2)
}

int main(){
    // 阶乘
    printf("10的阶乘是:%d", factorial(10));
    // 斐波那契
    printf("第10项的斐波那契数是:%d", Fibonacci(10));
}

循环语句

while 语句

# include <stdio.h>

int factorial(int n){
	int result = 1;
	while (n > 0) {
		result = result * n;
		n = n - 1;
	}
	return result;
}

int main(){
    printf(factorial(10));
    return 0;
}

do while 语句

# include <stdio.h>
int factorial(int n){
	int result = 1;
	int i = 1;
	do {
		result = result * i;
		i = i + 1;
	} while (i <= n);

	return result;
}

int main(){
    printf(factorial(10));
    return 0;
}

for 语句

# include <stdio.h>
int factorial(int n){
	int result = 1;
	int i;
	for(i = 1; i <= n; ++i)
		result = result * i;
	return result;
}

int main(){
    printf(factorial(10));
    return 0;
}

break 和 continue

#include <stdio.h>

int is_prime(int n){
	int i;
	for (i = 2; i < n; i++)
		if (n % i == 0)
			break;
	if (i == n)
		return 1;
	else
		return 0;
}

int main(void){
	int i;
	for (i = 1; i <= 100; i++) {
		if (!is_prime(i))
			continue;
		printf("%d\n", i);
	}
	return 0;
}

结构体

复合类型与结构体

根据语法规则由基本类型组合而成的类型称为 复合类型(Compound Type),例如字符串是由很多字符组成的。

现在,我们用 C语言来表示一个复数:从直角坐标系来看,复数由实部和虚部组成;从极座标系来看,复数由模和辐角组成,两种座标系可以相互转换。

# include <stdio.h>

struct complex_struct{
    double x, y;
}z1, z2;

int main(){
   	// 结构体字段赋值
    z1.x = 3.0;
    z1.y = 4.0;
    
 	// 结构体的初始化
   	struct complex_struct z3 = {0};
}

数据抽象

用 C 语言实现一个完整的复数运算程序。

# include <stdio.h>
# include <math.h>


struct complex_struct{
    double x, y;
};

double real_part(struct complex_struct z){
    return z.x;
}

double img_part(struct complex_struct z){
    return z.y;
}

double magnitude(struct complex_struct z){
    return sqrt(z.x*z.x + z.y*z.y);
}

double angle(struct complex_struct z){
    return atan2(z.y, z.x)
}

struct complex_struct make_from_real_img(double x, double y){
	struct complex_struct z;
	z.x = x;
	z.y = y;
	return z;
}

struct complex_struct make_from_mag_ang(double r, double A){
	struct complex_struct z;
	z.x = r * cos(A);
	z.y = r * sin(A);
	return z;
}

struct complex_struct add_complex(struct complex_struct z1, struct complex_struct z2)
{
	return make_from_real_img(real_part(z1) + real_part(z2),
				  img_part(z1) + img_part(z2));
}

struct complex_struct sub_complex(struct complex_struct z1, struct complex_struct z2){
	return make_from_real_img(real_part(z1) - real_part(z2),
				  img_part(z1) - img_part(z2));
}

struct complex_struct mul_complex(struct complex_struct z1, struct complex_struct z2){
	return make_from_mag_ang(magnitude(z1) * magnitude(z2),
				 angle(z1) + angle(z2));
}

struct complex_struct div_complex(struct complex_struct z1, struct complex_struct z2){
	return make_from_mag_ang(magnitude(z1) / magnitude(z2),
				 angle(z1) - angle(z2));
}


int main(){
    return 0;
}

数据类型标志

通过枚举类型,定义复数的类型。

# include <stdio.h>

enum coordinate_type { RECTANGULAR, POLAR };
struct complex_struct {
	enum coordinate_type t;
	double a, b;
};

int main(){
    return 0;
}

数组

数组的基本概念

数组(Array) 也是一种复合数据类型,它由一系列相同类型的元素(Element)组成。它的特点就是这些元素是相邻存储的,并且其中的每个元素可以通过下标索引进行访问。

# include <stdio.h>

int main(void){
	int count[4] = { 3, 2, }, i;

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

我们做一个案例:要求成一列0~9的随机数保存在数组中,然后统计其中每个数字出现的次数并打印,检查这些数字的随机性如何。

# include <stdio.h>
# include <stdlib.h>
# define N 10000

int a[N];

void gen_random (int upper_bound){
    int i ;
    for (i = 0; i < N; i++)
        a[i] = rand() % upper_bound;
}

void print_random(){
   int i;
    for (i = 0; i < N; i++)
        printf("%d ", a[i]);
    printf("\n");
}

int howmany(int value){
	int count = 0, i;
	for (i = 0; i < N; i++)
		if (a[i] == value)
			++count;
	return count;
}


int main(){
    int i;
	gen_random(10);
	printf("value\thow many\n");
	for (i = 0; i < 10; i++)
		printf("%d\t%d\n", i, howmany(i));

    return 0;
}

然后我们使用 -E 可以看到预处理(Preprocess)阶段之后、编译之前的程序。

gcc -E main.c

这里预处理器做了两件事情,一是把头文件 stdio.hstdlib.h 在代码中展开,二是把 #define 定义的标识符 N 替换成它的定义20。

字符串

字符串可以看作一个数组,它的每个元素是字符型的。

# include <stdio.h>

int main(void){
    // 通过下标获取字符串的字符
    char c = "Hello, world.\n"[0];
    
    // 字符数组存储字符串
    char str1[10] = { 'H', 'e', 'l', 'l', 'o', '\0' };
    char str2[10] = "Hello, world.\n";
    char str3[] = "Hello, world.\n";
    
    print("字符是:%c", c);
    print("字符串是:%s", str3);
    return 0;
}

数组后面元素没有指定,就会自动初始化为 0,就是 Null 字符。当然,以 \0 为结尾的也叫字符串,所以只要是 Null 字符结尾,那么就是字符串。

多维数组

多维数组(Multi-dimensional Array) 就是在数组里面嵌套数组。

# include <stdio.h>

int main(void){
    // a -> a[0]、a[1]、a[2]
    int a[3][2] = { 1, 2, 3, 4, 5};
    
    int b[][2] = { { 1, 2 },
		{ 3, 4 },
		{ 5, } };
    
    return 0;
}

简单的小游戏 - 剪刀石头布

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

int main(void){
    char gesture[3][10] = {"剪刀", "石头", "布"};
    int man, computer, result, ret;
    
    srand(time(NULL));
    printf("现在开始剪刀石头布游戏!\n");
    while (1){
        computer = rand() % 3;
        printf("请选择(剪刀-0,石头-1,布-2):");
        ret = scanf("%d", &man);
        
       	if (ret != 1 || man < 0 || man > 2) {
			printf("无效输入! 请输入 0, 1 或 2.\n");
            sleep(3);
        	system("clear");
			continue;
		}
        
		printf("你出了: %s\t电脑出了: %s\n", 
			gesture[man], gesture[computer]);

		result = (man - computer + 4) % 3 - 1;
		if (result > 0)
			printf("你赢了!\n");
		else if (result == 0)
			printf("平局!\n");
		else
			printf("你输了!\n");
        sleep(3);
        system("clear");
    }
    
}

GDB调试

调试工具 gdb,可以完全操控程序的运行。

调试的基本思想就是“分析现象->假设错误原因->产生新的现象去验证假设”这样一个循环。

单步执行和跟踪函数调用

#include <stdio.h>

int add_range(int low, int high){
	int i, sum;
	for (i = low; i <= high; i++)
		sum = sum + i;
	return sum;
}

int main(void){
	int result[100];
	result[0] = add_range(1, 10);
	result[1] = add_range(1, 100);
	printf("result[0]=%d\nresult[1]=%d\n", result[0], result[1]);
	return 0;
}

在编译时要加上 -g 选项,生成的可执行文件才能用 gdb 进行源码级调试:

gcc -g main.c -o main

通过 gdb 命令进入调试:

gdb main

这里要说明的是,gdb 并不是将源码嵌入到可执行文件中的,它也是从外部获取源码来进行调试分析的。

我们通过 start 命令开始调试,gdb 会停在变量定义后的第一条语句等待我们的命令,可以使用 next 进行下一步语句:

(gdb) start
Temporary breakpoint 1 at 0x400568: file zz2.c, line 14.
Starting program: /root/testdir/zz2 
Temporary breakpoint 1, main () at zz2.c:14
14          result[0] = add_range(1, 10);
(gdb) next
15          result[1] = add_range(1, 100);

此外,我们还可以通过 step 命令就进入到函数的执行中:

(gdb) start
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Temporary breakpoint 2 at 0x400568: file zz2.c, line 14.
Starting program: /root/testdir/zz2 
Temporary breakpoint 2, main () at zz2.c:14
14          result[0] = add_range(1, 10);
(gdb) step
add_range (low=1, high=10) at zz2.c:5
5           int i, sum = 0;

使用 backtrace 命令可以查看函数调用的栈帧:

(gdb) backtrace
#0  add_range (low=1, high=10) at zz2.c:5
#1  0x0000000000400577 in main () at zz2.c:14

可见当前的 add_range 函数是被 main 函数调用的,main 传进来的参数是 low=1, high=10main 函数的栈帧编号为1,add_range 的栈帧编号为 0 。现在可以用 info 命令查看 add_range 函数局部变量的值:

(gdb) info locals
i = 32767
sum = -138851417

如果初始化有错误,我们也可以直接修改初始化变量:

(gdb) set sum = 0
(gdb) info locals
i = 32767
sum = 0

如果想查看 main 函数当前局部变量的值也可以做到,先用 frame 命令选择 1 号栈帧然后再查看局部变量:

(gdb) frame 1
#1  0x0000000000400570 in main () at zz2.c:14
14          result[0] = add_range(1, 10);
(gdb) info locals
result = {0, 0, 4, 0, 0, 0, -8264, 32767, -8304, 32767, 0, 0, -8208, 32767, -134223256, 32767, -134224160, 32767, -136425313, 32767, 0, 0, 
  -8208, 32767, 0, 0, 0, 0, 0, 832, -134224160, 32767, 960, 1472, 2496, 2496, 2496, 2496, 2496, 2496, 0, 0, -134223256, 32767, -8448, 32767, 
  -8464, 32767, 1700966438, 0, -138851417, 32767, 11538501, 0, -140227432, 32767, -8368, 32767, -138865760, 32767, 256, 64, 118, 0, -8368, 
  32767, 194, 0, -8272, 32767, -8256, 32767, 9, 0, -139772531, 32767, 118, 0, 0, 0, 0, 0, 15775231, 0, 1, 0, 4195837, 0, -8272, 32767, 0, 0, 
  4195760, 0, 4195392, 0, -8048, 32767, 0, 0}

通过 print 命令还可以直接打印变量的值:

(gdb) print sum
$1 = 0

最后,可以使用 finish 函数让程序运行到从当前函数返回为止:

(gdb) finish
Run till exit from #0  add_range (low=1, high=10) at zz2.c:7
0x0000000000400577 in main () at zz2.c:14
14          result[0] = add_range(1, 10);
Value returned is $2 = 55

断点

#include <stdio.h>

int main(void){
	int sum = 0, i = 0;
	char input[5];

	while (1) {
		scanf("%s", input);
		for (i = 0; input[i] != '\0'; i++)
			sum = sum*10 + input[i] - '0';
		printf("input=%d\n", sum);
	}
	return 0;
}

程序的作用是:首先从键盘读入一串数字存到字符数组 input 中,然后转换成整型存到 sum 中,然后打印出来,一直这样循环下去。

例如输入是 "2345" ,则循环累加的过程是 (((0*10+2)*10+3)*10+4)*10+5=2345

我们可以用 display 命令使得每次停下来的时候都显示当前sum的值,然后继续往下走:

(gdb) display sum
1: sum = 0
(gdb) next
10              scanf("%s", input);
1: sum = 0

undisplay 命令可以取消跟踪显示,变量 sum 的编号是1,可以用 undisplay 1 命令取消它的跟踪显示:

(gdb) n
10              scanf("%s", input);
1: sum = 123345
(gdb) undisplay 1

通过 break 命令在第 10 行打个断点,然后用 continue 连续运行到达断点:

(gdb) list 10
5           int sum = 0, i = 0;
6           char input[5];
7
8           while (1)
9           {
10              scanf("%s", input);
11              for (i = 0; input[i] != '\0'; i++)
12                  sum = sum * 10 + input[i] - '0';
13              printf("input=%d\n", sum);
14          }
(gdb) break 10
Breakpoint 2 at 0x4005b3: file zz3.c, line 10.
(gdb) continue
Continuing.
123
input=123345123
Breakpoint 2, main () at zz3.c:10
10              scanf("%s", input);

我们可以用 info 命令查看断点情况:

(gdb) info breakpoint
Num     Type           Disp Enb Address            What
2       breakpoint     keep y   0x00000000004005b3 in main at zz3.c:10
        breakpoint already hit 2 times

我们可以用 disable 命令禁用断点,用 enable 重新启用断点,用 delete 删除断点:

(gdb) disable breakpoints 2
(gdb) info breakpoints
Num     Type           Disp Enb Address            What
2       breakpoint     keep n   0x00000000004005b3 in main at zz3.c:10
        breakpoint already hit 2 times
(gdb) enable 2
(gdb) info breakpoints
Num     Type           Disp Enb Address            What
2       breakpoint     keep y   0x00000000004005b3 in main at zz3.c:10
        breakpoint already hit 2 times
(gdb) delete breakpoints
Delete all breakpoints? (y or n) t
Please answer y or n.
Delete all breakpoints? (y or n) y
(gdb) info breakpoints
No breakpoints or watchpoints.        

gdb 的断点功能非常灵活,还可以设置断点在满足某个条件时才激活,例如我们仍然在循环开头设置断点,但是仅当sum不等于0时才中断,然后用 run 命令重新从程序开头连续运行:

(gdb) break 10 if sum != 0
Breakpoint 3 at 0x4005b3: file zz3.c, line 10.
(gdb) i breakpoints
Num     Type           Disp Enb Address            What
3       breakpoint     keep y   0x00000000004005b3 in main at zz3.c:10
        stop only if sum != 0
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/testdir/zz3 
123
input=123

Breakpoint 3, main () at zz3.c:10
10              scanf("%s", input);
2: sum = 123        

想要退出 gbd 调试的话,只需要输入 quit 就可以了:

(gdb) quit
A debugging session is active.

        Inferior 1 [process 14749] will be killed.

Quit anyway? (y or n) y

观察点

#include <stdio.h>

int main(void)
{
	int sum = 0, i = 0;
	char input[5];

	while (1) {
		sum = 0;
		scanf("%s", input);
		for (i = 0; input[i] != '\0'; i++)
			sum = sum*10 + input[i] - '0';
		printf("input=%d\n", sum);
	}
	return 0;
}

如果我们想查看某个数的输出可以用 x 来输出:

(gdb) n
11              scanf("%s", input);
(gdb) n
12345
12              for (i = 0; input[i] != '\0'; i++)
(gdb) x/8b input
0x7fffffffdfa0: 0x31    0x32    0x33    0x34    0x35    0x00    0x00    0x00

如果我们想观察某个值的变化,可以用 watch 来设置观察点:

(gdb) watch input[0]
Hardware watchpoint 4: input[0]
(gdb) c
Continuing.
input=12345
234
Hardware watchpoint 4: input[0]

Old value = 49 '1'
New value = 50 '2'
0x00007ffff7a68382 in __GI__IO_vfscanf () from /lib64/libc.so.6

段错误

如果程序运行时出现段错误,用 gdb 可以很容易定位到究竟是哪一行引发的段错误:

#include <stdio.h>

int main(void)
{
	int man = 0;
	scanf("%d", man);
	return 0;
}

然后我们用 gdb 调试这段程序:

(gdb) run
Starting program: /root/testdir/zz5 
123

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7a69341 in __GI__IO_vfscanf () from /lib64/libc.so.6
(gdb) backtrace
#0  0x00007ffff7a69341 in __GI__IO_vfscanf () from /lib64/libc.so.6
#1  0x00007ffff7a790b9 in __isoc99_scanf () from /lib64/libc.so.6
#2  0x0000000000400580 in main () at zz5.c:6

gdb 显示段错误出现在 _IO_vfscanf 函数中,用 bt 命令可以看到这个函数是被我们的 scanf 函数调用的,所以是 scanf 这一行代码引发的段错误。仔细观察程序发现是 man 前面少了个 &

【】

Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy
© Licensed Under CC BY-NC-SA 4.0