linux C语言编程

gcc编译器

使用gcc编译 .c 文件

linux@ubuntu:~/work/temp$ gcc hello.c

默认生成可执行文件 a.out

linux@ubuntu:~/work/temp$ ls
a.out hello.c

a.out 为ELF格式文件

linux@ubuntu:~/work/temp$ file a.out 
a.out: ELF 64-bit LSB  executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0ca8e541c2a64cdfcb622a89ad8a554fd5f3d83c, not stripped

在linux下可直接运行a.out

linux@ubuntu:~/work/temp$ ./a.out 
hello world!

gcc编译过程详解

gcc编译 .c 文件时,实际上会经历以下4个步骤

1.预处理

.c 文件进行头文件展开以及宏替换,但不会校验代码的正确性

linux@ubuntu:~/work/temp$ gcc -E hello.c -o hello.i

2.编译

检查预处理过的文件中代码的正确性,编译生成汇编文件

linux@ubuntu:~/work/temp$ gcc -S hello.i -o hello.s

3.汇编

将汇编文件进行汇编,生成 .o 目标文件(ELF格式但不可执行)

linux@ubuntu:~/work/temp$ gcc -c hello.s -o hello.o 

4.链接

将目标文件(通常会有多个目标文件,包括动态库和静态库)链接生成可执行文件(ELF格式,在linux下可直接运行)

linux@ubuntu:~/work/temp$ gcc hello.o -o hello
linux@ubuntu:~/work/temp$ ./hello
hello world!

如何生成纯净的二进制 .bin 文件

linux@ubuntu:~/work/temp$ objcopy -O binary hello hello.bin

静态库和动态库

静态库:编译时需要,运行时不需要
优点:移植性强,编译完成后可以脱离静态库工作
缺点:编译生成的文件较大
动态库(共享库):编译时不需要,运行时需要
优点:不占文件大小,编译生成的文件较小
缺点:移植性较差

静态库

静态库文件格式:lib***.a
如何生成静态库

linux@ubuntu:~/work/temp$ gcc -c fun.c -o fun.o
linux@ubuntu:~/work/temp$ ar crs libfun.a fun.o

如何使用静态库

linux@ubuntu:~/work/temp$ gcc main.c  -L./lib  -lfun

参数 -L 指定静态库的路径 ./lib
参数 -l 链接库名 fun

动态库

动态库文件格式:lib***.so
如何生成动态库

linux@ubuntu:~/work/temp$ gcc -fPIC -c fun.c
linux@ubuntu:~/work/temp$ gcc -shared -o libfun.so fun.o

如何使用动态库
1.先将动态库放在指定目录下,一般是 /lib 或 /usr/lib

linux@ubuntu:~/work/temp$ sudo mv libfun.so  /usr/lib/

2.然后在编译源文件时链接动态库,生成可执行文件

linux@ubuntu:~/work/temp$ gcc main.c -lfun

注:此处只是单纯地链接动态库,并没有加载动态库的内容,只有程序运行时才会加载动态库。

3.在运行程序前需要指定动态库链接路径,否则会提示找不到动态库

linux@ubuntu:~/work/temp$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/lib
linux@ubuntu:~/work/temp$ ./a.out

变量和常量

常量:在程序运行过程中不会发生改变
变量:在程序运行时可以发生改变
全局变量:不在{ }内的变量
局部变量:在{ }内的变量
变量定义:[ 存储类型 ] [ 数据类型 ] [ 变量名 ]
定义:在内存上分配空间
声明:只是告诉编译器此变量存在有空间,并不是在声明处分配空间
变量初始化:在变量定义时对变量进行赋值

存储类型

auto:修饰局部变量,如果变量定义时没有指定存储类型,则默认为auto类型;
register(寄存器类型):把修饰的变量存放到寄存器中,提高程序运行效率,当寄存器满时默认变成auto类型;
extern(外部引用类型):引用同一工程中的全局变量;
static:作用如下
1、修饰全局变量,限制作用域,使该全局变量生效的空间仅在本文件中;
2、修饰函数,作用同上;
3、修饰局部变量,改变局部变量的存放位置,延长局部变量的生命周期,运算的结果保留上一次的结果,并且局部变量的重复定义无效。

内存分段

内核空间(3~4G)
用户空间(0~3G)
{
栈区:存放局部变量和函数形参
缓冲区:用于栈区向下拓展和堆区向上拓展,线程栈等
堆区:动态分配malloc、释放free
全局区(静态区):存放全局变量和static修饰的局部变量,又分为初始化数据(.data)和未初始化数据(.bss),未初始化数据默认为0值
代码区(常量区):存放程序代码(.init & .text)和常量(.rodata)
NULL:空地址区
}

数据类型

不同类型的数据大小和操作系统的关系

数据类型64位系统32位系统16位系统/8位单片机
char1byte1byte1byte
short2byte2byte2byte
int4byte4byte2byte
long8byte4byte4byte
float4byte4byte-
double8byte8byte-

数据越界

代码编译过程一般不能识别出数据越界问题,因此涉及到计算时要格外警惕此类问题出现;
例如
char i = 127+1;

char i = -128-1;
以上计算结果超过8bit大小,所以需要用更大范围的数据类型。

数据类型转换

隐式转换
场景一、赋值
如果赋值符号左右两边数据类型不同,进行赋值时可能出现:
1.浮点型赋给整形会造成精度丢失
2.大的数据类型赋给小的数据类型,可能造成数据越界
场景二、算术运算
1.如果运算时算式中的数据类型不一致,默认向最大的数据类型发生转变
2.当有符号数与无符号数运算时,会默认无符号进行运算
场景三、使用printf函数
显式转换
示例:
char i = 127;
int j = (int)i;
注意:
1.大的数据类型向小的数据类型转换时可能出现数据越界
2.浮点型向整形进行强制转换时可能会丢失精度
3.强制数据类型转换并不会改变数据本身

运算符

算术运算符

双目运算符
/ 取整
% 求余(仅用于整型运算)
单目运算符
“++” 自加
“- -” 自减
运算符前置和后置的区别

b = a++;    // 先赋值后运算 b = a, a = a+1; 
b = ++a;    // 先运算后赋值 a = a+1, b = a;  

关系运算符

逻辑运算符

&& 逻辑与
一般形式:
表达式1 && 表达式2
逢0截止:如果表达式1不成立,直接输出最终结果为0,不再判断表达式2。
|| 逻辑或
一般形式:
表达式1 || 表达式2
逢1截止:如果表达式1成立,直接输出最终结果为1,不再判断表达式2。

位运算符

左移"<<"
有符号数左移时不需要考虑符号位,符号位移到什么就是什么(逻辑左移)
右移">>"
右移时需要考虑符号位,假设补码右移n位,如果为负数,高位补n个1,如果为正数,高位补n个0(算术右移)
注意:对于有符号数到底是采用逻辑移位还是算术移位取决于编译器,因此如果程序中存在有符号数的右移操作,则该程序不可移植。

特殊运算符

“,” (逗号运算符)
一般形式:
x = (表达式1,表达式2,表达式3);
最终x的值为最后一个逗号后面的表达式3的运算结果,但前面的表达式1/2也可能会影响表达式3的运算结果。

“?” (条件运算符,属于三目运算符)
一般形式:
表达式1 ? 表达式2 : 表达式3
判断表达式1的结果是否成立,若成立则输出表达式2的结果,反之则输出表达式3的结果。

“&”(取址运算符)
获取变量a的首地址

&a

地址:内存是以一个字节(byte)为单位进行划分的,并且每个字节都有对应的编号,那么这个内存编号也就是所谓的地址。

“*”(指针运算符)
根据变量a的首地址获取变量a的值

a == *(&a);

变量交换

交换a,b变量值
int a = 10 ,b = 4;
1.中间变量

int c;
c = a;
a = b;
b = c;

2.算术交换(前提是a+b的值不能超过a和b的数据类型大小)

a = a+b;
b = a-b;
a = a-b;

3.异或交换(前提是a和b的值不能相同)

a = a^b;
b = a^b;
a = a^b;

原码和补码

在计算机中,一切数据以补码的形式存储和运算;同时规定正数的反码和补码均与原码相同,而负数的反码为原码除符号位不变其余数据位取反,补码为反码+1(包括符号位);以char类型的数据为例,有符号的8bit存储大小,最高位为符号位(1为负,0为正),取值范围在-128~127;
例如
char i = 127;
原码 0111 1111
反码 0111 1111
补码 0111 1111

例如
char i = -127;
原码 1111 1111
反码 1000 0000
补码 1000 0001

例如
char i = -128;
补码 1000 0000
因为数据大小限制在8bit,所以临界值-128只有补码,无法表示原码和反码
如果-128是9bit大小,那么其原码和补码相同
原码 1 1000 0000
反码 1 0111 1111
补码 1 1000 0000
因此8bit的-128可以理解为数据位和符号位重合

补码原码对照表

char补码反码原码
00000 00000000 00000000 0000
10000 00010000 00010000 0001
1270111 11110111 11110111 1111
-1281000 0000--
-1271000 00011000 00001111 1111
-11111 11111111 11101000 0001

数组

数组是若干变量(数据类型相同)的线性集合,属于构造类型,这些变量也叫作数组元素。
定义

char a[5];

释义:数组名a不仅表示数组变量,同时还是一个地址常量,该常量值是首元素a[0]的地址,也代表数组的首地址,即a == &a[0];数组a有5个元素,元素的数据类型为 char,但数组a的数据类型为

char [5]

所以数组的数据类型大小 = 元素的数据类型大小 * 元素个数;
初始化

char a[5] = {1,2,3,4,5}; // 下标5可省略

注:数组a只有元素 a[0]~a[4],如果引用 a[5] 属于数组越界,需要格外警惕,因为程序编译时一般不能识别出来。
字符数组的初始化

char a[] = "hello"; // 字符串
等价于
char a[] = {"hello"};
等价于
char a[] = {'h','e','l','l','o','\0'};
区别于
char a[] = {'h','e','l','l','o'}; // 非字符串

二维数组

一般形式:存储类型 数据类型 数组名[行标][列标]
定义

int b[3][4];

释义:b表示数组名,本身是一个地址常量,该常量值是第一行的地址,也代表二维数组的首地址;二维数组b有3*4个元素,元素的数据类型为 int,但数组b的数据类型为

int [3][4]

所以二维数组的数据类型大小 = 元素的数据类型大小 * 行数 * 列数。
行序优先:由于内存是线性的,所以二维数组在存储时会以行序优先的原则转化成一维来存储。这样二维数组可以看做是元素类型为数组的一维数组,即

int b[3][4] 相当于 int [4] b[3]; // 元素b[0]~b[2]是int [4]类型的数组

二维数组的完全初始化

int b[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
等价于
int b[][4] = {1,2,3,4,5,6,7,8,9,10,11,12}; // 行标可以省略

二维数组的不完全初始化

int b[3][4] = {{1,2},{3,4},{5,6}}; // 实际元素值为 1 2 0 0 | 3 4 0 0 | 5 6 0 0
区别于
int b[3][4] = {1,2,3,4,5,6}; // 实际元素值为 1 2 3 4 | 5 6 0 0 | 0 0 0 0

指针

初始化

int a = 10;
int *p = &a; // 初始化指针变量p,其存放变量a 的首地址,即指针p 指向变量a 
int b = *p; // 将指针p所指向的变量赋值给变量b,等同于 int b = a;
/** 注意 int *p = &a 中的符号"*"是指针标识符,
    而 int b = *p 中的符号"*"是指针运算符 **/

释义:指针变量p的数据类型为

int *

因为指针是用来存储地址的,所以不同的指针类型,其数据大小却是相同的,在32位系统中永远是4byte大小,而在64位系统中永远是8byte大小。

空指针

即指向0地址的指针,是合法的,但不能操作其指向的空间;

char *p = NULL; // NULL是编号为0的地址空间,受到系统保护

野指针

即没有明确指向的指针,是非法的,应当避免出现;

char *p; // 只定义了指针p却没有初始化

指针运算

指针加减

int a = 10; // 变量a为int类型,4byte大小
int *p = &a; // 指针p 指向变量a
int *q = p+1; // 指针q 指向以变量a首地址向高地址偏移4byte的空间
int *r = p-1; // 指针r 指向以变量a首地址向低地址偏移4byte的空间

示意图
指针加减运算可见指针的算术运算并非遵循普通的数值运算,而是地址偏移运算,并且偏移量是以指向的数据类型大小为基本单位。
指针相减运算

q-r == 2

前提:指针q,r必须指向同一片连续的空间,并且该空间内数据类型保持一致,相减结果为相差数据的个数。
指针关系运算

q > r  // 比较地址值的大小

二级指针

即指向指针的指针

int a = 10;
int *p = &a; // p为一级指针变量,指向变量a
int **q = &p; //q为二级指针变量,指向变量p
推导
q == &p;
*q == *(&p) == p == &a;
**q == *p == a;

数组与指针

在前面数组一章中提到过数组名的双重含义
例如定义一个数组

char a[5];

那么数组名a代表
1.一个类型为char [5]的数组变量a

char (*p)[5] = &a; // 定义一个char (*)[5]类型的数组指针 p,并指向数组变量a
区别于指针数组
char *r[5]; // 定义一个char *[5]类型的指针数组 r,其元素是char *类型的指针

2.一个地址常量(指针常量),地址值是该数组的首地址(首元素a[0]的地址)

a == &a[0]; // 等价关系
推导
a+1 == &a[0]+1 == &a[1];
推导
*(a+1) == *(&a[0]+1) == *(&a[1]) == a[1];
推导
*(a+n) == a[n];
定义一个char *类型的指针q,并指向数组a的首元素
char *q = a;
推导
q == a == &a[0];
推导
q+1 == a+1 == &a[1];
推导
*(q+1) == *(a+1) == *(&a[1]) == a[1] == q[1]
推导
*(q+n) == *(a+n) == a[n] == q[n]

注意:虽然 a == &a,但两者只是地址值相等,并不是等价关系,因为这里 a 是数组首地址,存储大小是 char 类型,而 &a 是数组变量的地址,存储大小是 char [5] 类型。
根据等式

*(&a) == a; // 等价关系

得出一个结论:当对整个数组空间地址进行指针运算时,其结果为该数组的首地址。

指针数组和二级指针

因为指针数组的元素是指针类型,而数组名又是数组的首地址,所以指针数组名实际上一个二级指针常量

char *r[5]; // 定义一个char *[5]类型的指针数组 r,其元素是char *类型的指针
char **p = r; // 定义一个二级指针变量p,指向指针数组 r的首地址

二维数组与指针

定义一个二维数组

char b[3][4];

二维数组名b是二维数组的首行地址,即

b == &b[0]; // b[0]代表第一行的数组元素,类型为 char [4]
推导
b+1 == &b[0]+1 == &b[1];
推导
*(b+1) == *(&b[1]) == b[1];

根据数组名的双重含义可知

b[0] == &b[0][0]; // b[0]也代表第一行的数组首地址,类型为char *
推导
b[1]+1 == &b[1][0]+1 == &b[1][1]; // 第二行数组的第二个元素地址
推导
*(b+1)+1 == b[1]+1 ==  &b[1][1];
推导
*(*(b+1)+1) == *(&b[1][1]) == b[1][1];
推导
*(*(b+i)+j) == b[i][j];

定义一个数组指针并指向二维数组的第一行数组

char (*p)[4] = b;
推导
p == b == &b[0];
推导
*(*(p+i)+j) == *(*(b+i)+j) == b[i][j] == p[i][j];

常量修饰符const

作用是修饰变量,使变量值不能被修改

const int a = 10; //const修饰变量a,其值不可更改
等价于
int const a = 10;
/* 此后不能再给a赋值 */
a = 11; // a的值不可变,错误的写法
/* 但是可以通过指针修改变量值 */
int *p = &a;
*p = 11; // *p的值可变,允许的用法

const修饰指针变量的用法

int * const p = &a; // p的值不可变,*p的值可变
const int *p = &a;
等价于
int const *p = &a; // p的值可变,*p的值不可变
const int * const p = &a; // p和*p的值都不可变

函数

函数声明
函数返回值类型 函数名(形式参数);

int fun(char a, char b);

函数定义
函数返回值类型 函数名(形式参数)
{
C语言语句;
return 返回值;
}

int fun(char a, char b)
{
    int c = a*b;
    return c;
}

函数调用
函数名(实际参数);

char i = 10,j = 11;
fun(i,j);

形式参数是在函数执行时临时创建的局部变量,作用域在函数空间;函数传参时将实际参数的值赋给形式参数。

函数与指针

函数指针:指向函数的指针,因为函数名本身就是函数的入口地址

/* 定义函数fun */
int fun(char a, char b)
{
}
/* 定义函数指针p,并指向函数fun */
int (*p)(char, char) = fun; // 返回值和参数的类型必须保持一致
/* 通过指针p调用函数*/
p(i,j); // 等价于 fun(i,j);

指针函数:返回值为指针类型的函数,需要警惕不能返回函数空间内的地址,因为随着函数执行结束,其所有空间会被释放。

/* 定义指针函数fun */
int *fun(char *p, char b)
{
    char a = 10;
    *p = b*a;
    return p; // p指向的空间地址由外部传入
}

main函数

int main(int argc, const char *argv[])
{
	printf("argc:%d\n",argc);
	printf("argv[0]:%s\n",argv[0]);
	printf("argv[1]:%s\n",argv[1]);
	printf("argv[2]:%s\n",argv[2]);
	return 0;
}

默认参数
int argc:执行时命令行传参的个数,包括执行命令(例如./a.out)
const char * argv[]:存放命令行传参的内容,均为字符串常量,argv[0] 一般为 “./xxx”
例如在命令行执行a.out时

linux@ubuntu:~/work/temp$ ./a.out hccsdn 12345

则 argc == 3
argv[0] == “./a.out”
argv[1] == “hccsdn”
argv[2] == “12345”

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值