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);
- 在库<stdlib.h>中,作用是开辟一个新的进程执行(调用
fork()
),来执行参数字符串所代表的命令。
#include <stdio.h> #include <stdlib.h> // gcc 中有这个库 #include <windows.h> int main() { printf("before\n"); // 系统调用 system("dir"); // 调用外部程序 system("hello.exe"); printf("after\n"); // 使用 windows提供的库函数,调用windows操作系统的功能 WinExec("calc", SW_NORMAL); return 0; }
- 在库<stdlib.h>中,作用是开辟一个新的进程执行(调用
- 宏
- 宏定义,会在预编译时展开
// 定义一个宏 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 "<built-in>" # 1 "<command-line>" # 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 <stdio.h> 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 <stdio.h> 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 <stdio.h> #include <stdlib.h> #include <time.h> 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;如果 s1<s2 则返回小于 0;如果 s1>s2 则返回大于 0。依次比较每个字符的ASCII码。 -
strncmp(s1, s2, n)
:可以指定比较长度n
。 -
strcat(s1, s2)
,字符串拼接。 -
sprintf(buf, "%d %d %d", a, b, c)
,将数据(a,b,c)以指定格式填充到buf
中。
#include <stdio.h> #include <string.h> 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 <stdio.h> #include <string.h> 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 <stdio.h> // 无参 无返回值 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 <stdio.h> // 这里是函数的声明 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 <stdio.h> 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 <stdio.h>
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 <stdio.h>
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 <stdio.h>
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 <stdio.h>
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 <stdio.h>
int a; // 全局变量 在 bss区(未初始化全局变量)
int *func(){
return &a;
}
int main(int argc, char const *argv[])
{
*func() = 100;
printf("a = %d\n", a);
return 0;
}
作用域
- 局部作用域
- 在
{}
内定义的变量只能在该局部作用域内使用,在代码执行到这个变量的定义时,编译器才会为这个变量分配空间。 - 在不同的作用域中可以定义同名的变量。
#include <stdio.h> 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 <stdio.h> 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 <stdio.h> 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 <stdio.h> #include <stdlib.h> 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 <stdio.h>
#include <string.h>
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 <stdio.h>
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 <stdio.h>
#include <string.h>
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 <stdio.h>
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 <stdio.h>
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 <stdio.h>
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 <stdio.h>
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 <stdio.h>
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 <stdio.h>
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 <stdio.h>
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 <stdio.h>
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())等刷新缓存区函数、程序关闭时才从缓冲区读写文件。
网友评论