编译
gcc
gcc -o main main.c
# 然后执行 main 文件
./main
# (或者)编译+链接
gcc -o main.o main.c -c
gcc -o main main.o
include
#include <stdio.h> // 表示文件在系统目录下
#include "a.h" // 表示文件在当前目录下
知识
- main函数是执行的起点,有且只能有1个
- printf 是向标准设备输出,未必是向屏幕输出,例如
./main > save.txt
- 建议用
int main
,而不是void main
,后者不支持c++ - 约定
return 0;
代表执行成功,return -1;
代表执行失败 - 预编译:把 include 替换进来,删除注释,等操作。
gcc -o main1.c main.c -E
- 汇编
gcc -o main.s main.c -S
基本数据结构
一个二进制位是一个bit,8 bit=1 Byte
c 本身不规定变量类型的范围,跟系统有关,我的 MacBook 如下:
修饰符 | 数据类型 | 占用(字节) | 取值范围 |
---|---|---|---|
signed | short int | 2 | -32768 到 32767 (-2^15 ~ 2^15-1) |
int | 4 | -2147483648 到 2147483647 (-2^31 ~ 2^31-1) | |
long int long long int |
8 | ||
unsigned | short int | 2 | 0 到 65535 (0 ~ 2^16-1) |
int | 4 | 0 到 4294967295 (0 ~ 2^32-1) | |
long int | 8 | ||
float | 4 | 1.4E-45 ~ 3.4E+38,-1.4E-45 ~ -3.4E+38 | |
duble | 8 | 4.9E-324 ~ 1.7E+308, -4.9E-324 ~ -1.7E+308 | |
char | 1 | -128~127 | |
unsigned char | 1 | 0~255 |
#include <stdio.h>
#include <float.h>
int main() {
printf("int 存储大小 : %lu \n", sizeof(int));
printf("a 存储大小 : %lu \n", sizeof(a));
printf("float 存储最节数 : %lu \n", sizeof(float));
printf("float 最小值: %E\n", FLT_MIN);
printf("float 最大值: %E\n", FLT_MAX);
printf("精度值: %d\n", FLT_DIG);
return 0;
}
编码
- 原码:最高位0代表正,1代表负
- 反码:如果是正数,反码和原码相同。如果是负数,符号位为1,其它各位与原码相反
- 补码:正数与原码相同。负数:反码加一
- 好处:相加不用再考虑正负
- 负数补码转原码:符号位不动,其它求反,得到的数加一
- 内存中以补码的形式存储
进制
int a;
// 以 n 进制赋值
a = 10; // 默认十进制
a = 010; //0开头为8进制
a = 0b101; // 0b 开头为 2 进制
a = 0x2f;// 0x开头为16进制
// 以 n 进制显示
printf("十进制 = %d,八进制 = %o, "
"十六进制小写 = %x , 16进制大写 = %X, "
"十进制无符号 %u", a, a, a, a, a);
char c = 0x41;
printf("size = %lu, value= %c", sizeof(c), c);
小数
float f = 123.4f;
double d1 = 100.1; // C语言默认都是 double
char ch = 'A';
printf("字符:%c,对应的ascii:%d\n", ch, ch);
类型转换
int x = 1;
double y;
y = (double) x;
字符串
// char 的 \b 是退格符号,例如:
printf("hello word");
char b='\b';
printf("%c%c",b,b);
// 输出 hello wo
// 字符串 s/S char/wchar
printf("%s\n", "hello");
// p 16进制指针
int a = 1;
printf("%p\n", &a);
// 输出一个百分号
printf("%%", );
// printf 还有对其之类的操作,就不多写了
常量:不可被修改
// 宏常量,规范:用大写
#define MAX 100;
// const 常量
const int a = 0;
// c用宏常量多,C++用const多
"字符串常量"
20 // 整数常量
volatile,不让编译器自动做优化,例如:
int a =1;
a = a+1;
a = a+2;
// 以上代码会被编译器自动优化为: a = a+3
volatile int a = 1
// 编译器不会再自动做优化,
register:提升效率
register int a = 0; // 这个变量直接放在寄存器中,而不是内存中
a = a + 1;
// 对应汇编
move eax, 0
add eax, 1
// 普通的:
int a = 0;
a = a + 1;
// 对应汇编
move b, 0
move eax, b
add eax, 1
move b, eax
知识:
- register 是建议型指令,而不是命令型的。如果寄存器不够用,register 可能不生效
复杂类型
struct // 结构体
union // 共用体
enum // 枚举
typedef // 声明类型别名
sizeof // 得到类型大小,32位系统返回 unsigned int,64位系统返回 unsigned long
#include <stdio.h>
enum month {
JAN = 1, FEB = 2, MAR = 3, APR = 4, MAY = 5, JUN = 6,
JUL = 7, AUG = 8, SEP = 9, OCT = 10, NOV = 11, DEC = 12
};
int main() {
enum month lastmonth, thismonth, nextmonth;
lastmonth = APR;
thismonth = MAY;
nextmonth = JUN;
printf("%d %d %d \n", lastmonth, thismonth, nextmonth);
return 0;
}
小知识:
- 如果不定义值,第0个默认为0,后面的是前面的加1
enum month { JAN = 1, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC };
运算符
+-*/ 加减乘除
% 取模
i++ , i-- 自加/自减
=,+=,-=,*=,/=,%= 赋值
int x, y, z;
x = y = z = 5; //为三个变量同时赋值
// 原理是 (z = 5) 除了赋值外还会返回5
// 所以 if(z=5) 不会报错,如果本意是 if(z == 5),会有问题
逻辑运算符
C语言没有bool类型,使用 0 代表假,非 0 代表真
// 比较运算符,返回0或1
==,!=,<,>,<=,>=
// 与或非
&&,||,!
// 三目运算符
max = x > y ? x : y;
小知识:
- 逻辑运算符不会做额外运算,例如:
3>1||x/0
不会报错,因为第二部分就不会运算
位运算符
& 按位与
| 按位或
~ 按位反
^ 按异或
<< 移位
>> 移位
存储相关
auto
static
register
extern
const
volatile
流程相关
continue
break
goto
选择语句
if语句
// 第一种
if (/* condition */) {
/* code */
} else if (/* condition */) {
/* code */
} else {
/* code */
}
// 1. else if 和 else 都可以省略
// 2. if(z=5) 不会报错,如果本意是 if(z == 5),会有问题
// 3. if(a==b); 多一个分号也不会报错,只不过会直接结束if,后面的代码不在 if 范围中了。
// 第二种
if (/* condition */) {
/* code */
}
// 第三种
if (/* condition */) {
/* code */
} else {
/* code */
}
// 第四种
if (/* condition */) {
/* code */
} else if (/* condition */) {
/* code */
} else {
/* code */
}
switch
int a;
printf("input a=");
scanf("%d", &a);
switch (a) {
case 0:
printf("input 0");
break;
case 1:
printf(" and 1");
break;
case 2:
printf("other");
break;
default:
printf("not known!");
}
// 1. break 必须带,不然的话,匹配成功一个,下面的每个 case 都会执行(不知道为啥要这样设计)
循环结构
for (size_t i = 0; i < count; i++) {
/* code */
}
do {
/* code */
} while(/* condition */);
while (/* condition */) {
/* code */
}
goto:不要用
goto label1;
printf("hello");
label1:
printf("world");
关键词:
continue; // 直接执行下一个循环
break; // 跳出循环
数组
int a[10] // 定义了一个数组,其长度为10
// 1. 数组的元素类型必须都一样
// 2. 数组名本身是数组第一个元素的地址对应的常量
printf("%p == %p", a, &a[0]); // 两个都是在内存中的开头位置
// 初始化
int a[3] = {1, 2, 3};
int a[3] = {1, 2}; // 没定义的,默认为0
int a[] = {1, 2, 3}; // 如果不指定长度,自动计算和指定长度
// 3. 语法上,index 可以溢出,例如
a[100] = 999;
a[101];
// 4. 如何得到数组的长度? sizeof(a)/sizeof(a[0])
// 所以这样遍历数组
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++) {
printf("%d\n", a[i]);
}
二维数组
int a[2][5]; // 定义了一个二维数组,a[0],a[1] 是两个1维数组名
printf("%p = %p = %p", a, a[0], &a[0][0]); // 同一维数组,存放的也是内存地址
// 初始化
int a[2][5] = { {1, 2, 3, 4, 5},
{3, 4, 5, 6, 7} };
// 省略的自动补0
int a[2][5] = { {1, 2, 3, 4, 5},
{3} };
// 可以省略第一个大小
int a[][5] = { {1, 2, 3, 4, 5},
{3} };
多维数组
int a[2][3][5]; // 定义了一个三维数组,其 a[0], a[1] 分别代表2个二维数组
字符相关
字符数组
char a[2];
a[0] = 'a';
a[1] = '\0'; // 它的 ascii 码就是 0,所以也可以 a[1] = 0;
char a1[10] = "abc"; // 这其实是初始化一个字符数组。不能用赋值 a="abc" 是错的
char a2[] = "abc"; // 会多一位 \0,这个例子实际上长度为 4
字符数组和字符串的区别:
- 字符串是一种连特殊的
char []
,它必须以0结尾 char[]
如果中间某个值为0,字符串就到此截断,但整体仍然是个数组char a[] = "hello, world"; a[3] = 0; printf("%s", a); // 打印 hel
- 如果一个 char[] 没有以0结尾,它就不是个字符串,printf会出乱码
字符串数组和指针
char a[100] = "hello world";
char *s = "hello world"; // 这个是先定义一个 字符串常量,然后用指针指向它的首地址。所以是只读。
例题:如何合并两个字符串?
char a[100] = {0};
char b[100] = {0};
char c[200] = {0};
scanf("%s", a); // 这里不是 &a
scanf("%s", b); // 如果用户的输出超过100个,会有溢出
int idx1, idx2;
idx1 = idx2 = 0;
while (a[idx1]) {
c[idx1] = a[idx1];
idx1++;
}
while (b[idx2]) {
c[idx1] = b[idx2];
idx1++;
idx2++;
}
printf("%s", c);
字符串scan和print
printf
- %s 输出一个字符串
- %c 输出一个字符
- %d 以十进制输出一个有符号整型
- %u 以十进制输出一个无符号整型
- %o 以八进制输出一个整数
- %x 以十六进制输出一个小写整数
- %X 以十六进制输出一个大写整数
- %f 以十进制输出一个浮点数
- %e 以科学计数法输出一个小写浮点数
- %E 以科学计数法输出一个大写浮点数
scanf 有一些问题:
- 认为空格和回车,以及其它终止符都是结束,终止符号:
0x20
空格\t
水平制表符(tab键)\n
换行\v
垂直制表符\f
换页\r
回车
- 如果
char [10] a
,然后 scanf 输入长度为3的字符串,会添加0之后赋值给前几个元素,后面的元素保持不变。 - 如果用户输入超过 a 的长度,会溢出
putchar('a') \\ 一次输出一个字符
printf("hello") \\ 不多说
getchar() \\ 返回 int,是你输入的字符对应的 ascii 码
// scanf:
int a;
scanf("%d", &a); // 第二个变量是变量的地址
printf("%d\n",a);
- gets 和 puts 底层是由 putchar/getchar 实现的
char a[10];
gets(a);
// 1. 用户输出超长后会报错
// 2. 空格不代表结束,回车代表结束
// 3. 仍然有溢出危险
fgets(a, sizeof(a), stdin);
// 1. 用户超长后不报错,而是截断
// 2. 回车和空格都会放进去
// 3. 没有溢出危险
puts(a); // 跟 printf 功能差不多,自动加一个 \n,所以和 gets 连用会出现两次 \n
fputs(a, stdout);
// 1. 不会自作主张加 \n
string.h
- 还有一类print/scan
// 字符串格式化:sprintf
char a[100];
sprintf(a, "%d: Hello, %s, welcome.", 1, "Tom");
// 把格式化后的字符串,放入a
// 可以这么理解:printf 是输出到标准输出设备,sprintf 是输出到字符数组
// 额外一个小应用:int 转 char[]
// 从指定格式中的字符中提取:sscanf
char a[] = "12+21";
int i, j;
sscanf(a, "%d+%d", &i, &j);
// 额外一个小应用:char[] 转 int(其实也能转 double)
// 也可以 char c;sscanf(a, "%d%c%d", &i, &c, &j);
// 字符串中提取数字
char a[100] = "105hellohello";
char *stopstring;
long int i = strtol(a, &stopstring, 6);
// 1. 以 6 进制提取
// 2. stopstring 是提取后的剩余部分
// 3. 类似的有这些:strtof, strtol, strtold, strtold, strtoll 等
// 4. 类似的还有 atoi, atof 等
// 5. 没有反过来转化的内置函数,可以用 sprintf 实现
场景 | 输出 | 输入 | 备注 |
---|---|---|---|
字符,std_io | putchar('a') | int a = getchar(); | |
字符,文件 | char c=getc(p); | putc(text[i], p); | c==EOF为终止条件 |
同上 | fgetc(p) | fputc(p) | |
字符串,std_io | char a[10]; gets(a); | puts(a) | 有溢出风险,回车也有问题,不要用 |
字符串,std_io | char a[10]; fgets(a, sizeof(a), stdin); | fputs(a, stdout); | 建议使用 fgets会提取末尾的换行符,fputs不会自动添加换行符 |
字符串,文件 | char buf[1024] = {}; fgets(buf, sizeof(buf), p); // 这里一次读取一行 | fputs(a, p); | feof(p)=1 判断到了结尾 |
字符串格式化,std_io | scanf("%d", a); 不会把换行符号加入到a中 | printf("%d\n",a); | 不要提取%s,会有问题 |
字符串格式化,char array | char a[] = "12+21"; int i, j; sscanf(a, "%d+%d", &i, &j); | sprintf(a, "%d: Hello, %s, welcome.", 1, "Tom"); | 同上的问题 |
字符串格式化,文件 | fscanf(p1, "%d%c%d=\n", &a, &b, &c); | fprintf(p2, "%d%c%d=%d\n", a, b, c, a + c); | 同上的问题 feof(p)=1 判断到了结尾 |
常用字符串方法
#include<string.h>
字符串长度:strlen
unsigned long len = strlen(a);
// 1. 这个长度不包括末尾的 0
// 2. 1个汉字算3个
// 3. 到第一个0为止,既是数组后面还有内容
字符串合并:strcat,strncat
strcat(a, b); // 把 a 和 b 合并,并且放到 a
// a 必须有足够的空间,否则报错
strncat(a, b, 3); // 最多追加 b 的前 3 个字符
字符串拷贝:strcpy,strncpy
strcpy(a, b); // 把 b copy 到 a 中
// 如果超过 a 的大小,不会报错,而是会继续写入后续的内存(或许会导致错误)
strncpy(a, b, sizeof(a) - 1);
// 最多只复制 sizeof(a) - 1 个,可以防止溢出
字符串比较:strcmp,strncmp
int is_unequal = strcmp(a, b);
// 如果不同,返回非0。如果相同,返回0
int is_unequal = strncmp(a, b, 2);
// 比价前2个字符串
// a == b 是不对的,因为这个比较的是内存
字符串查找:strchr,strstr
char a[] = "hello world!";
char *s; //定一个一个 char类型指针变量
s = strchr(a, 'w');
// 1. s 也是一个 char[]
// 2. printf("%s", s); 输出 world!,也就是遇到 \0 才结束
// 3. 如果找不到,返回 null,用 s==NULL 做判断
s = strstr(a, "wor"); // 功能相似,入参可以是字符串
字符串分割:strtok
char a[100] = "abc_efg_123_666666";
char *s;
s = strtok(a, "_"); // 返回 abc
printf("%s\n", s);
s = strtok(NULL, "_");// 继续查找,返回 efg。如果查完了,返回null
printf("%s\n", s);
// 使用示例:
char a[100] = "abc_efg_123_666666";
char *s;
s = strtok(a, "_");
while (s) {
printf("%s\n", s);
s = strtok(NULL, "_");
}
char的一些有关技巧
// 小写转大写的技巧
char a = 'a';
if (a >= 'a' && a <= 'z') { // 1. char 可以当成 int 用
a -= ' '; // 2. 减空格就代表着转大写(ascii设计不错)
}
// 同样原理,可以字符转数字
int a = '1';
if (a >= '0' && a <= '9') {
a -= '0';
}
函数
- 使用前必须先定义或声明。可以先声明,然后在底下定义。
- 任何位置执行
exit(0)
会让整个程序中止 - 字符数组作入参是这样的
int my_func(char *a)
多文件编译
// my_utils.c:
int my_add(int a, int b) {
return a + b;
}
// main.c:
#include<stdio.h>
int my_add(int a, int b);
int main() {
int a = 1;
int b = 2;
int c = my_add(a, b);
printf("%d\n", c);
return 0;
}
// 编译:
// gcc -o my_package main.c my_utils.c
// 执行:
// ./my_package
往往这么做
// my_utils.h
int my_add(int a, int b) {
return a + b;
}
// main.c
#include<stdio.h>
#include "my_utils.h"
int main() {
int a = 1;
int b = 2;
int c = my_add(a, b);
printf("%d\n", c);
return 0;
}
// gcc -o my_package main.c
// ./my_package
另外,好像还有个规范:.h 文件只声明函数,函数定义放到 my_utils.c
宏
#include<stdio.h>
#define TEST// 定义了一个宏
int main() {
#ifdef TEST // 如果定义了 TEST,就编译这里面的,否则不编译
printf("dev");
#endif
#ifndef TEST
printf("prod") // 如果没定义就不编译,定义了就编译
#endif
return 0;
}
宏的应用,在一个头文件中,一般这么做,以防重复 include 时,被重复声明(类似单例模式)
#ifndef _MY_UTILS
#define _MY_UTILS
int my_add(int a,int b){
return a + b;
}
#endif
还有一个规范:.h
文件一般只放函数的声明,.c
文件放函数的定义
一个错误的使用:
#include <stdio.h>
int func1();
int main() {
func1(); // 这里编译器不会提示错误,但会产生一个随机的值
return 0;
}
int func1(int a){
printf("%d",a);
return 0;
}
对于真的无入参函数,规范:使用 int func1(void);
,C++ 没有这个问题。
标准库-内存操作
string.h
memset:内存置空
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
memset(a, 0, sizeof(a));
// 入参:置空的区域的首地址,0,这块内存大小(单位 Byte)
memcopy:内存拷贝。要确保没有内存重叠区域
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int b[10] = {0};
memcpy(b, a, sizeof(a));
// 入参:目标地址,源地址,拷贝的大小(单位 Byte)
标准库
随机数
#include<time.h>
#include<stdlib.h>
srand(100); // 指定种子
int a = rand(); // 如果不定义种子,不会随机定种子
// 如何得到真随机:
int t=(int) time(NULL);
srand(t);
int a = rand();
// 如何得到0-100的随机数?
rand() % 101;
system
#include <stdlib.h>
int main()
{
system("ls -l"); // 运行成功返回0
return 0;
}
system 返回什么?
- 被调用者的返回
- 如果调用 linux 命令,成功返回0
- 如果调用其它编译好的 another.c,返回 another.c#main 函数的 return (???并不是)