gcc的简单使用
gcc -E hello.c -o hello.i
预处理.c文件,处理文件包含,宏定义,条件编译
gcc -S hello.i -o hello.s
编译,将预处理过的文件编译成汇编文件。过程包括:词法分析,语法分析,语意分析,代码生成等。
gcc -c hello.s - o hello.o
汇编,生成目标文件,此时是二进制代码。目标文件没有链接动态库,还不是可执行文件。
静态库生成(ar -rc libxxx.a xxx1.o xxx2.o xxx3.o),静态库以.a后缀结尾
动态库的生成(gcc -fPIC -shared xxx1.c xxx2.c xxx3.c -o libxxx.so),动态库以.so后缀结尾
链接:目标文件必须链接过后才可以执行
静态链接:gcc -o hello hello.o libxxx.a
动态链接:gcc -o hello hello.o libxxx.so
lld.hello.exe:在linux中可以通过shell脚本 lld 查看可执行模块的dependency
基本语法
system函数:int system(const char * string);
在库中,作用是开辟一个新的进程执行(调用fork()),来执行参数字符串所代表的命令。
#include
#include
// gcc 中有这个库
#include
int main()
{
printf("before\n");
// 系统调用
system("dir");
// 调用外部程序
system("hello.exe");
printf("after\n");
// 使用 windows提供的库函数,调用windows操作系统的功能
WinExec("calc", SW_NORMAL);
return 0;
}
宏
宏定义,会在预编译时展开
// 定义一个宏 main.c
#define NUM 100
int main(int argc, char const *argv[])
{
int a;
// 预处理会展开宏
a = NUM;
return 0;
}
// 使用 `gcc -E` 预编译后 main.i
# 1 "main.c"
# 1 ""
# 1 ""
# 1 "main.c"
int main(int argc, char const *argv[])
{
int a;
100 = 10;
a = 100;
return 0;
}
进制表示
0x:十六进制,0:八进制
输出时,%d:有符号十进制,%u无符号十进制输出,%o:无符号八进制,%x:无符号十六进制,输出时,以格式化输出的类型来决定输出的类型,输出时都是以四字节输出。在以%u,%o,%x输出时,如果原数据最高位是1,则高位补1。
char b = 0x81;
printf("%x\n", b); // 输出ffffff81
计算机在存储时,都是以二进制补码形式存储,且每种类型的第一位为补码,注意在以%d输出时的进制转换问题。
char b = 0x81;
printf("%x\n", b); //输出 -127
// 1000 0001 char 类型是8位存储,第一位为符号位
// 原码为 1111 1110 + 1 = 1111 1111 = -127
如果以小容量类型存储大容量类型的数据,超出部分被舍弃。以大容量数存储小容量数据,如果最高位是1,则在前面全部补1补齐位数。
数据类型:每种数据类型都有一定的存储大小,如果超过了存储空间大小,多出的位数则会舍弃,所以在给一种类型赋值或计算时,应注意该数据类型的取值范围,如果越界,可能产生意料之外的结果。
整型
char、unsigned char:占一个字节。'a'为字符常量,打印格式为%c;"a"为字符串(字符数组),打印格式为%s,每个字符串结尾,编译器都会自动添加一个结束标志位 \0,两者不一样。-128 到 127 或 0 到 255。
short、unsigned short类型,存储整数,2字节
int、unsigned int类型,存储整数,4字节
long、unsigned long,4字节
浮点型
float,4字节,精度是六位小数
double,8字节,精度15位小数
long double,16字节,精度19位小数
数组
声明
int arr[2] = {1, 2}
数组越界:编译时不会出错,但是当程序运行时,如果用到了存储越界数组项这块空间时,会报错。
数组名指向数组的首地址。c语言里面没有获取数组长度的方法,sizeof(arr) / sizeof(arr[0])可以获取数组长度。
多维数组:int arr[2][3] = { { 1, 2, 3 },{ 4, 5, 6 } }
字符数组和字符串:字符串以\0 或 0结尾。字符串可以以%s格式输出,原因是字符串输出指针需要结束符0。
#include
int main(int argc, char const *argv[])
{
char ch[] = { 'h', 'e', 'l', 'l', 'o' }; // 字符数组,长度是5
char str2 = "hello" // 这种方式初始化,默认加 \0 结束符,所以长度是6
char str[] = { 'h', 'e', 'l', 'l', 'o' '\0'}; // 字符串
char str1[] = { 'h', 'e', 'l', 'l', 'o' 0}; // 字符串
return 0;
}
流程控制s
for(continue,break,goto)
#include
int main(int argc, char const *argv[])
{
goto next; // 跳转到同一个作用域的标记处
printf("zzzz1\n");
printf("zzzz2\n");
next:
printf("123\n"); // 执行这里
return 0;
}
while
do...while
库函数
随机数
#include
#include
#include
int main(int argc, char const *argv[])
{
/*
1. srand函数是随机数发生器的初始化函数。
2. 原型:void srand(unsigned int seed),如果传入参数相同,则产生随机数也相同。
3. srand和rand()配合使用产生伪随机数序列。
*/
srand((unsigned int) time(NULL));
int i = 0;
int num;
for(i = 0; i < 5; i++)
{
// rand()函数返回一个随机数
num = rand();
printf("%d\n", num);
}
return 0;
}
字符串处理函数
每个字符串常量都是一个地址,指向字符串的首元素地址。字符串存储在_data的文字常量区,相同的字符串拥有相同地址。
gets(char []):获取一个键盘输入的字符串,以回车结束,和 scanf() 不同的是它可以读取空格,这个函数不安全,已经弃用。
fgets(char *, size, *stream);:可以指定读取长度,换行符也会读取。
strlen(s1):返回字符串s1的长度。和sizeof()不同的是,strlen()不包括结束符\0的长度,而且遇到\0结束。
strcpy(dst, src):字符串拷贝,以首元素开始,遇到\0结束。
strncpy(dst, src, length):字符串拷贝,可以拷贝\0,但是不能拷贝\0后面的数据。
strcmp(s1, s2):如果 s1 和 s2 是相同的,则返回 0;如果 s1s2 则返回大于 0。依次比较每个字符的ASCII码。
strncmp(s1, s2, n):可以指定比较长度n。
strcat(s1, s2),字符串拼接。
sprintf(buf, "%d %d %d", a, b, c),将数据(a,b,c)以指定格式填充到buf中。
#include
#include
int main(int argc, char const *argv[])
{
int a, b , c;
char buf[100];
a = 1, b = 2, c = 3;
sprintf(buf, "%d %d %d", a, b, c);
printf("buf = %s\n", buf); // buf = 1 2 3
return 0;
}
sscanf(buf, "%d %d %d", a, b, c):将buf中的数据以指定格式提取到(a,b,c)中。注意格式要严格一致。
#include
#include
int main(int argc, char const *argv[])
{
int a, b , c;
char buf[] = "a = 1, b = 2, c = 3";
sscanf(buf, "a = %d, b = %d, c = %d", &a, &b, &c);
printf("a = %d b = %d c = %d\n", a,b,c); // a = 1 b = 2 c = 3
return 0;
}
strchr(s1, ch):返回一个指针,指向字符串 s1 中字符 ch 的第一次出现的位置。
strstr(s1, s2):返回一个指针,指向字符串 s1 中字符串 s2 的第一次出现的位置。
char *strtok(char *str, const char *delim):该函数返回被分解的第一个子字符串,如果没有可检索的字符串,则返回一个空指针,该函数会破坏原字符串,在匹配的位置加上\0。第二次调用时,第一个参数写NULL =>char *strtok(NULL, const char *delim),切割的还是第一次调用被破坏的字符串。
函数
库函数,使用头文件引入,然后使用。
自定义函数。
#include
// 无参 无返回值
void hello()
{
printf("hello func\n");
}
// 含参无返回值
void add(int a, int b){
printf("%d\n", a + b);
}
// 含参有返回值
int multiply(int a, int b){
return a * b;
}
int main(int argc, char const *argv[])
{
hello();
add(1, 2);
printf("2 * 3 = %d\n", multiply(2, 3));
return 0;
}
函数的声明和定义: 在main函数中调用其他函数时,只会往前去找函数的定义,如果没有定义就找函数的声明, 如果声明也没有,c编译器会报警告,c++会报错。所以在调用函数之前,一定要进行函数的声明,声明时形参名可以不写。
#include
// 这里是函数的声明
int func(int a, int b);
int main(int argc, char const *argv[])
{
func(1, 2);
return 0;
}
// 这里是函数的定义
int func(int a, int b) {
return a + b;
}
封装库函数。在.c文件中编写自定义函数,在.h文件中写函数声明,在用到这些函数时,先将.h文件包含进来,(用双引号引入),将这些c文件一起打包编译。
.h文件为了防止被多次包含,可以在第一行加入。
// 1. 条件编译
#ifndef MY_STRLEN
#define MY_STRLEN
extern int my_strlen(char arr[]);
#endif
// 2. 或者在第一行加入
#pragma once
指针
指针是一个变量,其值为另一个变量的地址,即,内存位置的直接地址。就像其他变量或常量一样,您必须在使用指针存储其他变量地址之前,对其进行声明。
指针也是一种数据类型。
指针指向谁,就把谁的地址赋值给指针
直接操作指针变量本身没有意义,需要操作*p---指针所指向的内存。
指针的大小是由编译器决定的。
#include
int main(int argc, char const *argv[])
{
int *p;
int a = 10;
p = &a;
printf("%p %p\n", p, &a); // 0061FF28 0061FF28
*p = 10;
printf("%d %d\n", a, *p); // 10 10
return 0;
}
野指针:指针保存了一个非法地址。(只有定义变量后系统分配的地址才是合法的),操作野指针时不会操作,但是在操作野指针指向的内存时,由于系统在该进程中未对改地址授权,会报一个段错误。
空指针:给指针变量赋值为NULL。
int *p = NULL;
// 相当于是
int *p;
p = NULL;
多级指针:指向指针的指针是一种多级间接寻址的形式,或者说是一个指针链。通常,一个指针包含一个变量的地址。当我们定义一个指向指针的指针时,第一个指针包含了第二个指针的地址,第二个指针指向包含实际值的位置。
#include
int main(int argc, char const *argv[])
{
int a = 10;
int *p = &a;
int **q = &p;
*p = 20;
**q = 30;
// a= 30 *p = 30 **q = 30
printf("a= %d *p = %d **q = %d \n ", a, *p, **q);
return 0;
}
指针操作方式
#include
int main(int argc, char const *argv[])
{
int *p;
int a = 10;
p = &a;
// a = 10 *p = 10 *(p+0) = 10 p[0] = 10
printf("a = %d *p = %d *(p+0) = %d p[0] = %d\n", a, *p, *(p+0), p[0]);
return 0;
}
void类型指针
#include
int main(int argc, char const *argv[])
{
int a = 10;
/*
1. 不能用 void 定义变量,因为无法确定变量要分配的内存大小
2. 但是可以用 void 关键字定义指针,因为指针的大小是固定的,根据编译器来确定。
3. 在使用 void 关键字定义的指针是,需要进行类型适配。
* 理由是地址指向的都是这个数据的首地址(int 4 字节的首地址),操作整个4
字节的数据,但是 void 类型没有大小无法确定操作多大空间。
*/
void *p;
p = &a;
//printf("*p = %d\n", *p); // error: invalid use of void expression
printf("*p = %d\n", *(int *)p); // 把 void 类型的指针转换成 int 类型的指针
return 0;
}
指针的步长,指针的加减法根据指针的步长来进行加减。指针的步长由指针的类型决定,比如int类型的指针步长是4。
#include
int main(int argc, char const *argv[])
{
int a = 10;
int *p = &a;
printf("p = %p P+1 = %p\n", p, p+1); // p = 0061FF28 P+1 = 0061FF2C
return 0;
}
指针与数组
数组存储的是指针类型的数据,每个数据是一个指针。
int *p[3];
形参中的数组:形参中的数组不是数组,是一个指针。
func(int a[100]){} // 形参大小是一个指针的大小
func(int a[]){} // 形参大小是一个指针的大小
func(int *a){} // 形参大小是一个指针的大小
数组名是一个指针,指向数组第一个元素的地址,可以利用指针步长的特性来遍历数组。
指针作为函数返回值:注意,不能返回一个局部变量的地址,局部变量在函数调用完成时会自动销毁。如果再用一个指针去接收这个地址会产生一个野指针。
#include
int a; // 全局变量 在 bss区(未初始化全局变量)
int *func(){
return &a;
}
int main(int argc, char const *argv[])
{
*func() = 100;
printf("a = %d\n", a);
return 0;
}
作用域
局部作用域
在{}内定义的变量只能在该局部作用域内使用,在代码执行到这个变量的定义时,编译器才会为这个变量分配空间。
在不同的作用域中可以定义同名的变量。
#include
int main(int argc, char const *argv[])
{
int a = 1;
if(1) {
int a = 2;
printf("%d\n", a); // 2
}
return 0;
}
static关键字声明的静态局部变量在程序编译的时候就会分配地址空间(_data区),不能用局部变量初始化,静态局部变量在程序退出时销毁。static初始化的局部变量只会初始化一次,但是可以赋值多次。
#include
void func(){
static int a = 1;
a++;
printf("%d\n", a);
}
int main(int argc, char const *argv[])
{
func(); // 2
func(); // 3
func(); // 4
return 0;
}
全局作用域
全局变量,在编译时分配地址空间,如果没有初始化,默认值为0,在程序(进程)结束时销毁,提前使用(定义前使用)需要先声明,可以声明多次。
#include
void func(){
extern int a; // 在这之前未定义,需要声明
printf("func = %d\n", a);
}
int a = 10;
void func1(){
printf("func1 = %d\n", a);
}
void func2(){
int a = 20;
printf("func2 = %d\n", a);
}
int main(int argc, char const *argv[])
{
func();
func1();
func2();
return 0;
}
static全局变量(包括函数),和普通全局变量的区别就是作用域不同,只在本文件内可用。
内存分配
在程序执行前,有几个分区的内存已经确定,可以在linux使用size命令查看。
text(代码区):一般只读,函数。
data:已初始化的数据,全局变量,static变量,文字常量区(只读)。
bss:未初始化数据,全局变量,static变量
目标文件段大小
在程序运行时除了加载上述已确定的内存外,还加载包括堆区内存和栈区内存。
栈内存(stack):存放普通局部变量,自动管理内存。
查看栈空间
堆内存(heap):手动申请空间,手动释放或程序结束系统释放,使用函数malloc(int size)申请堆内存。
#include
#include
int main(int argc, char const *argv[])
{
int *p;
p = (int *)malloc(sizeof(int));
if(NULL == p){
return -1; // 空间分配失败
}
*p = 100;
printf("%d\n", *p); // 100
printf("%p\n", p); // 00FF2A48
if(NULL != p){
// 释放 p 所指向的内存, p 中存储的地址信息不变,这个地址不能再被使用。
free(p);
p = NULL;
}
//*p = 20; // err
return 0;
}
}
结构体
定义和声明
#include
#include
struct Student
{
char name[50];
int age;
};
struct Teacher // 定义并声明
{
char name[50];
int age;
}t1, t2 = { "hgz",18 };
struct // 定义匿名结构体并声明
{
char name[50];
int age;
}w1 = { "hgz",18 }, w2;
int main(int argc, char const *argv[])
{
struct Student stu1 = { "hgz",18 };
struct Student stu2;
strcpy(stu2.name, "hgz");
stu2.age = 18;
struct Student stu3;
struct Student *p;
p = &stu3;
strcpy(p->name, "hgz");
p->age = 18;
printf("%s\n", "ok");
return 0;
}
结构体数组
struct Student stu[2] = {
{"hgz", 18},
{"zzz", 19}
};
结构体直接作为参数传递是值传递,如果需要修改原结构体,需要用地址传递(参数为结构体指针)。
#include
struct Student
{
int age;
};
void func(struct Student *p){
p->age = 20;
}
int main(int argc, char const *argv[])
{
struct Student s1 = {18};
func(&s1);
printf("%d\n", s1.age);
return 0;
}
结构体中的指针。(要先给地址再使用,可以给文字常量区字符串地址,栈地址或者堆区地址)
#include
#include
struct Student
{
char *name;
int age;
};
int main(int argc, char const *argv[])
{
// 相当于 s1.name = "zzz"; 将data区文字常量的字符串的首地址赋值给指针
struct Student s1 = {"zzz", 18};
printf("s1 = %s %d\n", s1.name, s1.age);
struct Student s2;
// strcpy(s2.name, "hgz"); // err 野指针 s2.name 还没有地址
char name[100];
s2.name = name; // 分配栈区空间
strcpy(s2.name, "hgz");
s2.age = 20;
printf("s2 = %s %d\n", s2.name, s2.age);
struct Student s3;
s3.name = (char *)malloc(strlen("hgzzz")); // 分配堆区空间
strcpy(s3.name, "hgzzz");
s3.age = 22;
printf("s3 = %s %d\n", s3.name, s3.age);
if(NULL != s3.name){ // 注意使用堆区地址时,使用完要释放
free(s3.name);
s3.name = NULL;
}
return 0;
}
共用体
共用体所有成员公用一块内存地址,内存大小为最大成员内存大小。所有成员都指向首地址。
#include
union Test{
unsigned int a;
unsigned short b;
unsigned char c;
};
int main(int argc, char const *argv[])
{
union Test t1;
// &t1.a = 0061FF2C &t1.b = 0061FF2C &t1.c = 0061FF2C 共用体成员指向同一块地址头
printf("&t1.a = %p &t1.b = %p &t1.c = %p\n", &t1.a, &t1.b, &t1.c);
t1.a = 0x44332211;
// t1.a = 44332211 t1.b = 2211 t1.c = 11 共用体公用一块内存
printf("t1.a = %x t1.b = %x t1.c = %x\n", t1.a, t1.b, t1.c);
t1.c = 0xaa;
// t1.a = 443322aa t1.b = 22aa t1.c = aa 修改共用体中的成员可能会影响其他成员
printf("t1.a = %x t1.b = %x t1.c = %x\n", t1.a, t1.b, t1.c);
return 0;
}
枚举类型
定义
#include
enum{ // 不初始化赋值默认为 0 开始递增
pink, red, yellow
};
int main(int argc, char const *argv[])
{
printf("%d\n", pink == 0 ); // 1
printf("%d\n", red == 1 ); // 1
printf("%d\n", yellow == 2 ); // 1
return 0;
}
typedef关键字:给一个已存在的类型取一个别名。
#include
int main(int argc, char const *argv[])
{
typedef int int32; // 当前使用的是32位gcc编译器
typedef struct Student
{
int32 age; // typedef替换发生在编译阶段 宏定义是在预编译阶段
}Student;
Student s1 = {18};
printf("%d\n", s1.age);
return 0;
}
文件操作
FILE 结构体:我们在进行文件操作时要用到这个结构体,在我们调用fopen(const char * filename, const char * mode)时,会返回一个指向堆区的指针(这个指针指向结构体的地址,和文件没有直接关系)。这个函数会初始化结构体成员,这些成员和文件有着联系,当我们在进行文件操作时,实际上是这些成员在操作。
typedef struct{
short level; // 缓存区‘满’或者‘空’的程度
unsigned flags; // 文件状态标志
char fd; // 文件描述
unsigned char hold; // 如无缓冲区不读取字符
short bsize; // 缓冲区大小
unsigned char *buffer; // 数据缓冲区位置
unsigned ar; // 指针,在文件中的指向位置
unsigned istemp; // 临时文件指示器
short token; // 用于有效性检查
}FILE;
使用方式
#include
int main(int argc, char const *argv[])
{
// 第二个参数为 w 表示以写操作打开文件,如果文件不存在,创建文件,
// 如果存在,删除内容并打开
FILE *fp = NULL;
fp = fopen("b.txt", "w");
if(NULL == fp){
perror("fopen");
return -1;
}
// fputs(int a, FILE *stream)
char a = 'a';
while(a <= 'z'){
fputc(a, fp);
a++;
}
// 关闭文件
fclose(fp);
fp = NULL;
return 0;
}
标准文件输出
#include
int main(int argc, char const *argv[])
{
printf("%s\n", "zzzz"); // 标准输出(屏幕输出) zzzz
fclose(stdout); // 关闭标准输出文件指针
printf("%s\n", "hgz"); // 无输出
perror("abc"); // 打印函数调用失败的原因 abc : Bad file descriptor
return 0;
}
按块大小读写文件fread() fwrite()
#include
typedef struct Student
{
char name[50];
int age;
}Student;
int main(int argc, char const *argv[])
{
Student stus[4] = {
{ "hgz1", 18 },
{ "hgz2", 19 },
{ "hgz3", 20 },
{ "hgz4", 21 }
};
FILE *fp = fopen("6.txt", "w");
if(NULL == fp){
perror("fopen");
return -1;
}
int res = fwrite(stus, sizeof(stus), 1, fp);
printf("%d\n", res);
fclose(fp);
fp = NULL;
return 0;
}
#include
typedef struct Student
{
char name[50];
int age;
}Student;
int main(int argc, char const *argv[])
{
FILE *fp = fopen("6.txt", "r");
Student stus[10];
int res = fread(stus, sizeof(Student), 4, fp);
printf("res = %d\n", res);
for(int i = 0; i < 4; i++){
printf("%s %d\n", stus[i].name, stus[i].age);
}
fclose(fp);
return 0;
}
用fread() fwrite()实现文件拷贝命令
#include
int main(int argc, char const *argv[])
{
// 模拟linux cp 命令 -> cp a.txt b.txt 将a.txt 内容复制到b.txt中
if(argc != 3){
printf("%d\n", argc);
printf("argument count error:a.exe src dst\n");
return 0;
}
// 如果第二个参数是 r 在windows处理二进制文件会出问题
FILE *srcfp = fopen(argv[1], "rb"); // 源文件
FILE *dstfp = fopen(argv[2], "wb"); // 拷贝目的文件
char buf[4*1024];
int len; // 每次读取的长度
while(1){
len = fread(buf, 1, sizeof(buf), srcfp);
printf("len = %d\n", len);
if(len == 0){
break;
}
fwrite(buf, 1, len, dstfp);
};
fclose(dstfp);
fclose(srcfp);
return 0;
}
文件读写缓存区,在读写文件时,先把数据读写到缓冲区中。当缓存区满、调用(fclose()、fflush())等刷新缓存区函数、程序关闭时才从缓冲区读写文件。