0. 搭建运行环境
0.1 Windows
参考文末前两篇文章即可,编辑器使用vs code即可,比较轻量级
- 下载安装vs code
- 下载安装MinGW
官网:https://www.mingw-w64.org/downloads/
下载地址:https://sourceforge.net/projects/mingw/ - 配置环境变量
解压MinGW到安装目录,Path添加到xxx/bin,命令行运行:where gcc
检查配置效果
1. 变量
1.1 基本数据类型
直接上代码:
// c没有bool类型,但是c++有,注意区别
void testVariant()
{
int p1 = 1;
long p2 = 2L;
float p3 = 3.0;
double p4 = 4.0;
char p5 = 'a';
short p6 = 100;
printf("value = %d, sizeof p1 = %d\n", p1, sizeof(p1)); //value = 1, sizeof p1 = 4
printf("value = %ld, sizeof p2 = %d\n", p2, sizeof(p2)); //value = 2, sizeof p2 = 4
printf("value = %f, sizeof p3 = %d\n", p3, sizeof(p3)); //value = 3.000000, sizeof p3 = 4
printf("value = %lf, sizeof p4 = %d\n", p4, sizeof(p4)); //value = 4.000000, sizeof p4 = 8
printf("value = %c, sizeof p5 = %d\n", p5, sizeof(p5)); //value = a, sizeof p5 = 1
printf("value = %d, sizeof p6 = %d\n", p6, sizeof(p6)); //value = 100, sizeof p6 = 2
}
1.2 指针
1.2.1 一级指针
要点:
- 取址符
&
和 取值符*
- 指针要带数据类型的原因:1)确定如何取值;2)确定单位偏移量(针对数组指针);
示例:取址符&
和 取值符*
void testPointer()
{
int i = 100;
int *pi = &i;
int *pi2 = pi;
// 指针的基本概念
printf("pi = %#x, value = %d\n", pi, *pi); // pi = 0x61fe0c, value = 100
printf("pi2 = %#x, value = %d\n", pi2, *pi2); // pi2 = 0x61fe0c, value = 100,指针变量可以用指针变量接收
printf("pi3 = %p, value = %#x\n", &pi2, *(&pi2)); // pi3 = 0x61fe08, value = 0x61fe14,指针变量也是变量,所以也有地址
// 核心知识点:指针操作变量值
*pi = 200;
printf("i = %d, *pi2 = %d\n", i, *pi2); // i = 200, *pi2 = 200
}
示例:交换变量值
void swap(int *x, int *y)
{
int temp = *x;
*x = *y;
*y = temp;
}
示例:变量内存地址是不会发生改变的,下面这段交换值的函数将不生效
void swap_num_ptr(int* x, int* y)
{
int* tmp = x;
x = y;
y = tmp;
}
void swap_num_wrong(int x, int y)
{
int temp = x;
x = y;
y = temp;
}
1.2.2 多级指针
int num = 100;
int *p_num = #
int **pp_num = &p_num;
printf("num = %d",**pp_num);
1.2.3 函数指针
格式:返回类型(指针变量名)(形参类型)=函数名
函数指针可以作为接口使用
示例:函数指针作为形参类型
int add(int x, int y)
{
return x + y;
}
int minus(int x, int y)
{
return x - y;
}
int operate(int(method)(int, int), int x, int y)
{
return method(x, y);
}
int main()
{
int x = 3, y = 4;
int z = operate(add, x, y);
printf("z = %d\n", z);
z = operate(minus, x, y);
printf("z = %d\n", z);
return 0;
}
示例:函数指针变量的定义与赋值
int main()
{
int(*method)(int, int) = add; //
return 0;
}
示例:与数组类似,函数名其实就是函数指针
int main()
{
printf("add = %p, &add = %p",add,&add); //add = 00000000004017CE, &add = 00000000004017CE
return 0;
}
1.2.4 常量指针与指针常量
要点:常量指针不可修改变量值,指针常量不可修改指针值
// 常量指针(const在*之前)
int num = 11;
int num2 = 12;
const int* p_num=#
p_num = &num2; //允许对常量指针地址重新赋值
// *p_num = 13; //报错,不允许修改常量指针的值
// 指针常量(const在*之后)
int * const p_num2 = #
// p_num2 = &num2; //报错,指针常量不允许地址重新赋值
*p_num2 = 13; //允许修改指针常量的值
1.3 数组
要点:
- arr=&arr=&arr[0],都是数组第一个元素的地址
- 数组的内存地址是连续的,所以arr++是有意义的,表示arr的下个元素的地址
void testArray()
{
int arr[] = {0, 2, 3};
printf("arr = %p\n", arr);
printf("&arr = %p\n", &arr);
printf("&arr[0] = %p\n", &arr[0]);
// 第一种遍历数组方式
for (int i = 0; i < 3; i++)
{
printf("num = %d\n", arr[i]);
}
// 第二种遍历数组的方式
int *parr = arr; //这里为了演示arr的指针类型是int*才专门开辟一个指针变量接收,实际开发直接用arr即可
for (int i = 0; i < 3; i++)
{
printf("num = %d\n", *parr++);
printf("num = %d\n", *(parr+i));
printf("num = %d\n", parr[i]);
}
}
思考2:如何获取数组长度?
int arr[] = {0, 2, 3}; int len = sizeof(arr)/sizeof(int); //可以
int* arr = (int*)malloc(sizeof(int)*3); int len = sizeof(arr)/sizeof(int); //不可以,不能用指针变量获取数组长度
1.4 结构体
c的结构体跟Java的类(实体类)非常相似,区别是结构体构造方法只有默认的一个,无法像Java那样重载。支持内部结构体。
1.4.1 结构体的定义
// 普通结构体
struct Student
{
char name[10]; //ISO C++ forbids converting a string constant to 'char*'
int age;
double score;
};
// 定义同时初始化两个实例:两个实例的属性初始值默认是null或者0
struct Teacher
{
char name[10];
char subject[16];
Student students[1];
}t_wang={"王老师","math",{{"csfchh", 10, 100.0}}},t_li;
int main()
{
struct Student stu = {"csfchh", 10, 100.0};
printf("name = %s, age = %d, score = %ld\n", stu.name, stu.age, stu.score);
strcpy(stu.name,"hahha"); //无法直接修改,就是用字符串赋值
stu.age++;
printf("name = %s, age = %d, score = %ld\n", stu.name, stu.age, stu.score);
t_wang.name;
t_wang.subject;
t_wang.students[0].name;
return 0;
}
思考:结构体的内存大小如何计算?
字节补齐法则:当结构体中含有基本数据类型时,以最大的数据类型大小为单位,结构体总大小为这个单位的最小公倍数;如果结构体中不含基本数据类型?
例如Student=8*3=24
Teacher=56,怎么算?
1.4.2 结构体的使用
1.4.2.1 结构体创建
使用typedef
typedef Student STU; //将Student的别名定为STU,这样就可以用STU替代Student使用,用于简化代码,有些编译器不能直接使用结构体,需跟上struct关键词,用别名就可以绕开这个问题
typedef Teacher* TC, TEA; //别名可以取多个;对指针取别名就可以少敲一个*号
// 使用typedef避免编译器报错示例
// 普通结构体
typedef struct Student
{
char name[10]; //ISO C++ forbids converting a string constant to 'char*'
int age;//属性没赋值不能直接读
double score;
} Student;
创建结构体
// 创建结构体变量(一般不这么用)
struct Student stu = {"csfchh", 10, 100.0};
// 创建结构体指针(主流用法)
Student* p_stu = &stu; //c++,这种用法依赖于结构体变量
Student* p_stu2 = new Stuent(); //c++
Student* p_stu3 = (Student*)malloc(sizeof(Student));
if(p_stu3) free(p_stu3);
// 创建结构体数组
Student stus[3] = {{"aa",10,60.0}, {"bb",11,56.0},{}};
stu[2] = {"cc",10,60.0};
Student* stus2 = (Student*)malloc(sizeof(Student)*3);
strcpy(stus2->name,"aa"); //操作第一个结构体
stus2->age = 10;
stus2->score = 85;
stus2++; //后移一个单元长度,指向下一个结构体女
strcpy(stus2->name,"bb"); //操作第二个结构体
stus2->age = 11;
stus2->score = 56.0;
1.4.2.2 结构体访问
两种访问接头体属性的方法:使用结构体变量和使用结构体指针
int main()
{
struct Student stu = {"csfchh", 10, 100.0};
printf("name = %s, age = %d, score = %ld\n", stu.name, stu.age, stu.score);
// 使用结构体变量访问
strcpy(stu.name,"hahha"); //无法直接修改,就是用字符串赋值
stu.age++;
printf("name = %s, age = %d, score = %ld\n", stu.name, stu.age, stu.score);
// 使用指针访问结构体属性
Student *p_stu = &stu;
p_stu->age = 11;
return 0;
}
1.4 联合体
定义方式结构体一样,但是特性完全不同,联合体内的属性只有第一个访问的才有效,其他的均会被抛弃。
联合体的大小计算非常简单,等于属性最大值(还是要基本数据类型整除进行对齐)
typedef union Activity
{
int sleep;
double eat;
} Activity;
void main()
{
Activity act = {1, 2.0};
printf("sleep = %d, eat = %d\n", act.sleep,act.eat); //sleep = 1, eat = 1
}
1.5 枚举
枚举就是一个int,可以直接给枚举赋值一个整数,枚举默认第一个值是0
enum ContentType{
TEXT,IMAGE=1,OTHER
};
2. 函数
2.0 导包
首先说明一下导包的问题:
- 导系统包:
eg. #include <stdlib.h>- 导第三方包:
eg. #include “jni.h”
eg. #include “test.c”
#include导包实际上是将被导文件在编译时直接复制到目标文件中,这就存在循环引用问题,因此需要处理这个问题:
// 在被导的.h文件中增加下面的说明
#ifndef _included_pkgname_classname
#define _included_pkgname_classname
......
#endif
_included_pkgname_classname
是.h文件的自定义标识,需要保证唯一性,一般推荐的命名格式:
stdio.h -> STDIO_H
2.1 字符串<string.h>
2.1.1 字符串的表达
- char[]: eg. char str[]={‘h’,‘e’,‘l’,‘l’,‘o’,‘\0’}; //最后一个字符固定,必须是\0;
- char*: eg. char* str = “hello”;
区别是char[]是动态的,而char*是静态的。
2.1.2 长度strlen
字符串长度用int strlen(char* str)函数获取。
思考1:strlen如何获取字符串长度?
由于字符串以\0作为固定的结尾,那么只需要一个while循环判断当前数组元素是否是\0即可。
2.1.3 转换ato、strto
主要是ato和strto的系列方法:
void testStringTrans(){
char* str="12";
int v = atoi(str); //转成int,如果转换失败会返回0不会抛异常
str = "12.5";
float v2 = atof(str); //转换成float,如果转换失败返回0.000000
double v3 = strtod(str,NULL); //转换成float,如果转换失败返回0.000000
}
2.1.4 添加strcpy/strcat
- strcpy:从头开始覆盖
- strcat:从尾巴添加
2.1.5 截取
可以使用strcpy间接实现或者for循环自己拷贝
2.1.6 替换
需要考虑以下情况:
- 目标不存在;
- 字符串长度变化;
- 处理拼接逻辑;
- 存在多个目标;
2.1.7 查找
char* pos = strstr(char* str, char* goal); //返回goal首次出现的地址指针,否则返回null
2.1.8 比较strcmp
int rc = strcmp(char* str1, char* str2) 区分大小写
int rc = strcmpi(char* str1, char* str2) 不区分大小写
int rc = strncmp(char* str1, char* str2, int count) 区分大小写,比较前n个
rc等于0表示相等,rc大于0表示大于,rc小于0表示小于。
2.1.9 大小写转换
使用for循环遍历每个char,然后使用tolower(char c) 和 toupper(char c)进行转化
2.2 时间<time.h>
2.3 数学计算<math.h>
2.4 文件操作<stdio.h>
接口 | 说明 | 示例 |
---|---|---|
FILE* fopen(char* filepath,char* mode) | 打开文件 | |
void fclose(FILE* file) | 关闭文件 | |
int fread(int[] buffer, int stepsize, int stepcount, FILE* file) | 读取文件 | 以buffer为单位进行读 |
int fwrite(int[] buffer, int stepsize, int stepcount, FILE* file) | 写到文件 | 以buffer为单位进行写 |
int fgetc(FILE* file) | 读取一个字符 | 以单个字符为单位 |
int fputc(int c, FILE* file) | 写出一个字符 | 以单个字符为单位 |
2.4.1 读写文件
void testIO()
{
char *srcFilePath = "..\\lake.png";
char *dstFilePath = "..\\lake_copy.png";
FILE *src = fopen(srcFilePath, "rb");
FILE *dst = fopen(dstFilePath, "wb");
if (!src||!dst)
{
printf("can not open files");
return;
}
int SIZE=512;
int buffer[SIZE];
int len = -1;
int step = sizeof(int);
while ((len = fread(buffer, step, SIZE, src)) != 0)
{
printf("len = %d\n",len);
fwrite(buffer, step, len, dst);
}
fclose(src);
fclose(dst);
printf("copy finished");
}
2.4.2 获取文件大小
void testGetFileSize()
{
const char *srcFilePath = "..\\lake.png";
FILE *file = fopen(srcFilePath, "rb");
if (!file)
{
printf("can not open files");
return;
}
// 指针移动到文件末尾
fseek(file, 0, SEEK_END);
// 计算指针的偏移量就是文件大小
long walksize = ftell(file);
printf("file size is = %ld\n",(walksize/1024));
}
2.4.3 二进制文件简单加密
void testFileEncodeAndDecode()
{
int MASK = 56;
const char *srcFilePath = "..\\lake.png";
const char *encodeFilePath = "..\\lake_encode.png";
const char *decodeFilePath = "..\\lake_decode.png";
FILE *src = fopen(srcFilePath, "rb");
FILE *encode = fopen(encodeFilePath, "wb"); //如果不存在则会创建该文件
if (!src || !encode)
{
printf("can not open files");
return;
}
int SIZE = 512;
int buffer[SIZE];
int step = sizeof(int);
int len = -1;
// 这里是用fread+fwrite读写buffer
while ((len = fread(buffer, step, SIZE, src)) != 0)
{
buffer[0] ^= MASK; //核心原理是两次异或后数字还原
fwrite(buffer, step, len, encode);
}
//还可以使用fgetc+fputc读写单个字符,效率相对较低
// int c;
// while((c=fgetc(src))!=EOF){
// fputc(c^MAST, encode);
//}
// 关闭文件
fclose(src);
fclose(encode);
len = -1;
encode = fopen(encodeFilePath, "rb");
FILE *decode = fopen(decodeFilePath, "wb");
while ((len = fread(buffer, step, SIZE, encode)) != 0)
{
buffer[0] ^= MASK; //核心原理是两次异或后数字还原
fwrite(buffer, step, len, decode);
}
// 关闭文件
fclose(encode);
fclose(decode);
}
2.4.4 文件切割与合并
// 获取文件大小
long getFileSize(char *fileName)
{
FILE *file = fopen(fileName, "rb"); //read binary
if (!file)
return 0;
// 指针移动到文件末尾
fseek(file, 0, SEEK_END);
// 计算指针的偏移量就是文件大小
long file_size = ftell(file);
fclose(file);
return file_size;
}
// 合并文件
void joinFiles(char **fileNames, int fileCount)
{
const char *joinName = "..\\lake_join.png";
FILE *file = fopen(joinName, "ab"); //append binary
if (!file)
{
printf("can not write to joint file\n");
return;
}
FILE *temp;
int c = 0;
int SIZE=512;
int buffer[SIZE];
// int len;
int step=sizeof(int);
for (int i = 0; i < fileCount; i++)
{
printf("filename[%d] = %s\n", i, fileNames[i]);
temp = fopen(fileNames[i], "rb");
if (!temp)
{
printf("can not read from segment\n");
return;
}
// while ((len=fread(buffer,step,SIZE,temp))!=0)
// {
// printf("len = %d\n", len);
// fwrite(buffer, step, len, file);
// }
// len=0;
while ((c = fgetc(temp)) != -1)
{
printf("c = %d\n", c);
fputc(c, file);
}
fclose(temp);
}
fclose(file);
}
// 文件切割
int splitFile(char *fileName, char **fileNames,int fileCount)
{
long file_size = getFileSize(fileName);
if (file_size <= 0){
return 0;
}
FILE *file = fopen(fileName, "rb");
const long SPLIT_SIZE = file_size / fileCount;
FILE *temp;
long start = 0;
long end = 0;
int c = 0;
for (int i = 0; i < fileCount; i++)
{
printf("fileNames[%d] = %s\n", i, fileNames[i]);
// 计算分片的大小
start = i * SPLIT_SIZE;
if (i == fileCount - 1)
{
end = file_size;
}
else
{
end = (i + 1) * SPLIT_SIZE;
}
// 获取分片进行读写
temp = fopen(fileNames[i], "wb"); //write binary
if (!temp)
{
printf("can not write to segement\n");
return 0;
}
for (int j = start; j < end; j++)
{
fputc(fgetc(file), temp);
}
fclose(temp);
}
c = 0;
fclose(file);
return 1;
}
// 测试方法
void testFileSplitAndJoin()
{
char *fileName = "..\\lake.png";
int SPLIT_COUNT = 3;
char **fileNames = (char **)malloc(sizeof(char *) * SPLIT_COUNT);
for (int i = 0; i < SPLIT_COUNT; i++)
{
fileNames[i] = (char *)malloc(sizeof(char) * 30);
sprintf(fileNames[i], "..\\lake_%d.png", i);
}
// 先进行分片
if (splitFile(fileName, fileNames, SPLIT_COUNT))
{
// 再进行合并
joinFiles(fileNames, SPLIT_COUNT);
}
for (int i = 0; i < SPLIT_COUNT; i++)
free(fileNames[i]);
free(fileNames);
}
3. 内存管理
先看一个例子引出内存概念:
void test(){
int aar[10*1024*1024]; //直接申明,属于静态内存开辟
}
上面的代码开辟的内存空间为:(10 * 1024 * 1024 * 4+4) 个bit;
3.1 四区模型
内存名称 | 说明 | 内存开辟方式 |
---|---|---|
栈 | 存放函数的参数,局部变量值 | 最大内存空间2M,开辟内存方式为静态开辟(编译器自动分配) |
堆 | 存放程序员动态申请的变量(malloc、new等) | 最大内存空间为可用内存的80%,开辟内存方式为动态开辟,需要手动释放(free、delete) |
数据区 | 常量区(字符串常量等)、全局区/静态区(存放全局变量和静态变量) | 由操作系统负责开辟和释放 |
代码区 | 存放二进制代码 | 操作系统管理 |
3.2 动态内存管理
3.2.1 申请和释放<stdlib.h>
void testMalloc()
{
// 申请40M堆内存
int *mem = (int *)malloc(10 * 1024 * 1024 * sizeof(int));
// 释放堆内存
free(mem);
}
3.2.2 堆内存的使用
// 演示如何往动态内存写数据
void testMallocUsage()
{
int *mem = (int *)malloc(sizeof(int) * 5);
int i = 0;
for (; i < 5; i++) mem[i] = i;
free(mem);
}
3.2.3 内存再申请
允许对已申请的内存块调整大小,示例如下:
// 演示重新申请内存以修改动态内存大小
void testRealloc()
{
// 开始申请5个
int *mem = (int *)alloc(sizeof(int) * 5);
// 发现5个,不够用,再申请5个,共计10个
mem = realloc(mem, sizeof(int) * 5);
if (mem)//相当于if(mem!=NULL)
{
free(mem);
mem = NULL;
}
}
- alloc函数是从栈上分配内存无需释放;
- malloc是从堆内存分配内存需要释放;