文章目录
1. C语言基础
-
操作系统:管理硬件资源。
- 针对普通用户,图形界面.
- 针对开发人员,命令行窗口、系统调用。
-
我们的程序运行在操作系统基础之上,编写的程序依赖操作系统。
-
编译型语言、解释性语言。
- 编译型语言,例如 C语言。先把源代码全部编译成可执行文件,再执行。效率高。
- 解释性语言,例如 Python Lua Js 解释性语言。读一行解释执行一行。效率低。
-
C语言编译的过程: index.c
-
预处理: 处理预处理指令。简单文本替换。将 #define MAX 1024, 程序中所有使用 MAX 的地方
在预处理完之后,全部替换成 1024.
index.i
源代码文件 -
编译:
- 函数调用、变量使用做类型检查。
- 看看程序员有没有按照C语言语法要求编写代码。
- 代码优化。
index.s
汇编文件 -
汇编
- 把汇编代码转换成二进制目标文件。
index.o
-
链接
- 程序中用到很多库函数,负责找到这些函数,建立和这些函数连接。
- 添加启动代码。
- 目标文件、启动代码、库函数代码打包到一起,就变成 exe 文件。
-
2. 数据类型
-
变量类型
- 创建变量的时候,决定给变量分配多少内存。
- 编译器通过类型来确定运算规则。
-
整型变量:
- short 2个字节
- int 4 个字节
- long 4个字节(gcc 8字节)
- long long 8个字节
-
字符类型:
- 字符类型实际在内存中以 ASCII 码值(数字 0-255)
- 也可以把字符类型当做1字节长度的整型。
- -128-127, -0在程序中用-128来代替。
-
浮点型:
- float , 4 个字节, 6-7位,6位一定准,7位不一定准。
- double, 8个字节, 精确到 15-16位, 15一定准,16不一定准。
-
数值溢出:
- 有符号: C语言没有规定,未定义。VC编译器和有符号一样,char c = 127; c+=1; -128
- 无符号: C语言有规定。当这个数超过最大值,会回到原点。unsigned char c = 255; c += 1; 0
-
类型转换:
- 隐式类型转换。
- 显式类型转换(强制类型转转)。
- 尽量用强制类型转换,代码可读性好。
-
数据存储都是使用补码存储。
- 反码:符号位不变,其他位取反。
- 补码: 反码 + 1。
- 负数的补码:反码+1
- 正数的补码就是自身。
-
运算符:自增、自减运算
-
前置++:先加完再用。
-
后置++:先用再加。
int a = 10; int c = ++a; // 11 /* a = a + 1; int c = a; */ int c = a++; // 10 /* int c = a; a = a + 1; */
-
3. 流程控制
-
if语句语法:
if(条件) { } else if(条件) { } else if(条件) { } else { }
使用的时候,else if 不能单独使用。
-
三目运算符。
条件 ? 表达式 : 表达式; a > b ? a : b;
考试的时候,注意三母运算符嵌套。
主要用于替换简单的 if 语句。
-
switch 语句:
switch(变量) { case 值: 执行代码 break; case 值: 执行代码 break; default: break; }
-
循环语句:
-
while for 循环语法。
-
break 终止循环.
while(true) { while(true) { // 不会终止外层的循环 break; } }
-
continue 终止本次循环。
-
srand 设置随机数种子,以 time(NULL) 作为种子值. 默认种子值是 1.
-
rand 函数根据新的种子值计算产生一个随机数(伪随机)
-
4. 数组
-
数组三部分: 数组元素类型、数组名、数组元素个数.
int arr[3];
-
数组初始化
int arr[3] = {0}; // 部分初始化, 未初始化元素设置为 0 int arr[] = {10, 20, 30}; // 自动推到数组元素个数 char s[] = "hello world"; s[0] 获得 h s[1] 获得 e
-
数组注意点
- 数组下标越界
- 不能使用变量定义数组
- 数组定义之后无法修改大小
- 数组下标支持负数,但要注意越界
-
数组名的含义:指向数组首元素的指针常量(指针的指向不能修改)。
-
多维数组本质上是一位数组。
int arr[2][3]; int arr[2][3] = { {10, 20, 30}, {40, 50, 60} }; // 访问 // arr[0][1] 等等
-
数组做函数参数
void print_array(int arr[], int len){} // 数组名做函数参数 // 数组名作为函数参数的话,会退化为指针,此时 sizeof 无法计算整个数组长度 int arr1[] = {10, 20, 30}; void print_array(int arr[][3], int r, int c){} int arr2[2][3];
5. 字符串
-
字符串定义:
// 字符串常量: 用双引号括起来的字符串, 最后都会添加\0 "hello world"; // 1. 希望字符串从程序运行开始就创建,结束结束自动销毁 void test01() { // ”hello world“ 存储在字符串常量区,整个程序运行期间一直存在 // s 是函数内部的局部变量,函数结束之后,仍然会被销毁 char *s = "hello world"; } // 2. 希望字符串随着函数的执行结束就自动销毁 void test02() { // s[] 函数内部定义局部数组, 数组随着函数结束就会被销毁 // "hello world" 存储在字符串常量区 // ”hello world“ 会被拷贝一份放到 s 数组中 char s[] = "hello world"; } // 3. 希望能够控制变量生死 void test03() { // 给字符串开辟堆内存 char *s = malloc(strlen("hello world") + 1); if(NULL == s) { return; } // 使用 strcpy strncpy 可以将字符串数据拷贝到空间中 strcpy(s, "hello world"); // 打印字符串 printf("%s", s); // 释放空间 if(s != NULL) { free(s); s = NULL; } }
-
字符串操作
- strlen, 计算字符串长度,不算 \0 字符
- strcpy、strncpy, 进行字符串拷贝
- strcat、strncat , 进行字符串拼接
- strcmp、strncmp , 进行字符串比较
- sprintf , 格式化字符串
- atoi,将字符串转换为数字
6. 函数
-
函数返回值类型:
- 使用 return 关键字.
- 一个函数只能有一个返回值.
- 真正返回的值要和写的返回值类型要匹配。
- 函数是否有返回值要根据编写业务功能、需求来分析确定。
-
函数参数:
-
函数参数可以0个或者多个
-
函数如果没有参数,应该写 void
void func(void){}
-
函数调用时,函数需要几个参数,就要传递几个参数,不能省略。
-
函数在传递参数的时候是值传递。数组是地址传递。
-
-
函数体:
- 单一职责原则,函数尽量只做一件事,不要承担过多职责、
- 函数代码尽量少一些。
-
函数内部定义变量、函数形参叫做局部变量。 函数外部定义变量,叫做全局变量。
-
C分文件编写:
- .h 文件: 函数声明、变量声明
- .c 文件: 函数定义、变量定义
- 一般负责开发 C文件,负责编写 h文件
- 一般先写头文件,再写C文件
7. 指针
-
普通变量存储数据。指针用来存储变量的地址。为了语法上区分普通变量、指针变量,指针变量多一个星号。
-
知道这个变量是一个指针
-
知道这个变量指向的是什么类型
int a = 10; // *p 带了星号,表示p变量是一个指针变量 // int 表示指针保存的是一个 int 类型变量的地址 int *p = &a;
-
-
指针的操作
-
赋值。
int a = 10; int b = 20; int *p = &a; p = &b; // 赋值
-
解引用。
-
指针变量本身就是保存某一个变量的地址。
-
把指针变量存储的这个地址取出来。
-
找到这个地址的变量。
int a = 10; int b = 20; int *p = &a; *p = 100; // 表示修改p指向的a变量的值 int c = *p; // 把a变量的值赋值给c变量
-
-
步长操作。
- 指针步长,主要由指向的空间大小来决定。
- 比如指向的是 int 类型,int 类型空间 4个字节,步长就是4
- 比如指向的是 char 类型,步长就是1.
- char* *p; **char***四个字节, 所以 p+1, 加四个字节。
- 指针步长,主要由指向的空间大小来决定。
-
无类型指针。
-
不能进行步长运算。
-
不能解引用。
-
主要保存地址。
-
任何类型赋值给无类型指针不需要类型转换。
-
定义一个能够保存任何类型数据的数组:
void* arr[10]; // void* void* void*
-
-
const
-
const 修饰普通变量。变量不能修改。
int const a = 10; const int b = 20;
-
const 修饰指针变量。
int a = 10; // const 在星号左边,表示指向空间的值不能改。常量指针 const int *p = &a; // const 在星号左边,表示指向空间的值不能改 int const *p = &a; // 星号放在星号右边,指针指向不能改。 指针常量 int* const p = &a;
-
-
指针应用场景:
- 修改外部变量的值
- 提高数据传递效率
- 函数返回多个结果
void get_value(int arr[], int len,int *p_max, int *p_min, int *p_avg) { // 求最大值、最小值、平均值 int my_min = arr[0]; // 假设第一个元素最小值 int my_max = arr[0]; // 假设第一个元素最大值 int my_sum = 0; // 累加和 for (int i = 0; i < ARRAY_LENGTH; ++i) { // 记录最大值 if (arr[i] > my_max) { my_max = arr[i]; } // 记录最小值 if (arr[i] < my_min) { my_min = arr[i]; } // 累加元素和 my_sum += arr[i]; } // 计算平均值 int my_avg = my_sum / ARRAY_LENGTH; // 返回三个值 *p_max = my_max; *p_min = my_min; *p_avg = my_avg; }
-
多级指针:
int a = 10; int *p = &a; // 区分变量是一个指针,并且还要区分变量指向的数据类型是什么? int **pp = &p; int ***ppp = &pp;
多级指针解引用: 取地址+星, 解引用-星
int a = 10; int *p = &a; // 区分变量是一个指针,并且还要区分变量指向的数据类型是什么? int **pp = &p; int ***ppp = &pp; // *ppp 是二级指针类型 // **ppp 是一级指针类型
-
指针无论什么类型,无论几级指针,统统占4字节内存(32位)
-
8. 内存管理
9.语言第十天课程笔记
每一天的笔记包含如下内容:
- 当天授课内容安排
- 课堂重点内容笔记
- 课后思考题
9.1. 内容安排
第一节课: 作用域、变量分类(静态变量、非静态变量,静态函数、非静态函数)
第二节课: 内存分区(代码区、数据区-堆区、全局静态区、字符串常量区、栈区)
第三节课: 内存操作(malloc、free、memset、memcpy、memcmp、memmove)
第四节课: 结构体语法(结构体定义、结构体成员访问、结构体变量定义)
第五节课: 结构体使用注意、结构体作为函数参数
第六节课: typedef、enum
9.2. 作用域
作用域: 主要探讨标识符(函数名、变量名),这些名字在哪些范围内可以使用。变量名的可见范围.
函数只有文件作用域.
C作用域:
-
文件作用域. 例如: 有些变量在 a.c 定义的,在 b.c 就不能访问.
-
函数作用域. 例如: a函数内定义了一个变量
int number = 10
, b函数中就不能使用 a 函数定义的 number 变量. -
代码块作用域. 例如:
if(条件) { int a= 10; } if(条件) { printf("a = %d\n", a); }
9.3. 变量分类
- 非静态变量: 全局变量 局部变量
- 非静态全局变量
- 作用域:整个项目中都可以访问
- 内存管理: main 函数执行之前被创建,main 函数执行结束,内存回收.
- 非静态的局部变量
- 作用域: 该变量只在函数内可见
- 内存管理: 局部变量内存函数调用时,分配内存,函数调用结束,回收内存.
- 非静态全局变量
- 静态变量:静态全局变量,静态局部变量
- 静态全局变量
- 作用域: 文件作用域. 只能在当前 .c 文件内访问,在其他 .c 文件中不可访问,不可使用.
- 内存管理: main 函数执行之前创建,main函数执行结束之后回收.
- 静态局部变量
- 作用域: 静态局部变量也是一个局部变量,作用域是当前函数内部.
- 内存管理: main 函数执行之前创建,main函数执行结束之后回收.
- 静态全局变量
非静态变量:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
// 非静态变量: 全局变量 局部变量
// 非静态全局变量
// 作用域:整个项目中都可以访问
// 内存管理: main 函数执行之前被创建,main 函数执行结束,内存回收.
int g_number = 0;
void test01()
{
// 非静态的局部变量
// 作用域: 该变量只在函数内可见
// 内存管理: 局部变量内存函数调用时,分配内存,函数调用结束,回收内存.
int number = 0;
}
静态变量:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
// 静态变量:静态全局变量,静态局部变量
// s_number 静态全局变量
// 作用域: 文件作用域. 只能在当前 .c 文件内访问,在其他 .c 文件中不可访问,不可使用.
// 内存管理: main 函数执行之前创建,main函数执行结束之后回收.
static int s_number = 100;
void test01()
{
// 静态局部变量
// 作用域: 静态局部变量也是一个局部变量,作用域是当前函数内部.
// 内存管理: main 函数执行之前创建,main函数执行结束之后回收.
static int a = 100;
}
// 静态局部变量不会被销毁
void test02()
{
static int a = 10;
++a;
printf("a = %d\n", a);
}
// 1. 允许
// 2. 原因: a 变量是静态变量,整个程序运行期间一直存在.
int* get_number_pointer()
{
static int a = 100;
return &a;
}
// 全局变量、静态变量都是程序执行之前创建,程序执行之后销毁,整个程序运行期间,内存一直存在.
int main()
{
test02();
test02();
test02();
test02();
int *p = get_number_pointer();
system("pause");
return EXIT_SUCCESS;
}
9.4 函数分类
-
非静态函数
void func() { printf("hello world"); }
非静态函数也叫做全局函数,可以在整个项目任何的 .c 文件中访问.
-
静态函数
static void func() { printf("hello world"); }
静态函数只能在当前文件内访问.
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
// 访问在其他 .c 文件中定义的函数, 有两步:
// 1. 先声明该函数, 告诉编译器这个函数在其他文件中定义.
// 2. 调用函数.
// 如果 func 函数在其他文件中定义为静态函数,则在当前文件中不能使用.
void func(); // 函数声明
int main()
{
func();
system("pause");
return EXIT_SUCCESS;
}
9.5. 内存分区
编译器会将程序所使用的的内存。代码区、数据区。代码区放在只读区。
为什么代码放在只读区?为什么数据放在可读可写的区域呢?
代码一旦编写完毕,运行过程中不允许任意修改,为了保护代码,在设计的时候,代码就放在只读的内存区域。放代码的区域,叫做代码区。
数据区:
栈区:自动申请,自动释放。
- 函数的参数
- 函数内部定义局部变量
- 可读、可写
全局/静态区: 在程序执行之前分配内存,程序结束之后回收内存。
- 全局变量
- 静态全局变量
- 静态局部变量
- 可读、可写
字符串常量区: 程序运行之前创建,程序运行之后销毁.
- 双引号括起来的字符串会放在字符串常量区.
- 只读,不能修改.
堆区: 手动管理内存申请、手动释放内存。
1. 根据需要申请任意大小的内存。
2. 根据需要选择在合适的时间释放内存。
内存4区: 最主要目的是让我清楚,变量的生命周期。
栈区: 函数开始存储,函数结束释放。
全局静态区: 整个程序运行期间都会存在.
堆区: 手动申请,手动释放,如果申请了没有释放,会造成内存问题:内存泄露。
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
// 1. 申请和释放堆内存,需要两个底层函数 malloc free
void test01()
{
// 栈变量
int a = 10;
// 全局静态变量
static int b = 20;
// 向操作系统要了4字节内存,函数返回这个4个字节内存的首地址
// p 指向的内存 1. 手动释放 2. 程序结束之后,统一回收
int* p = (int *)malloc(4);
*p = 30; // 堆内存赋值
printf("*p = %d\n", *p);
// free 函数用于释放堆内存
// 当 free 调用之后,内存就不能再使用了。
// free(p);
}
// 2. 给double类型分配堆空间
void test02()
{
// 在堆上分配了8字节内存,用于存储 double 类型数据
double *d = (double *)malloc(sizeof(double));
// 如何写内存
*d = 3.14;
// 如何读内存
printf("*d = %lf\n", *d);
// 释放内存
free(d);
}
long* create_long()
{
// 动态申请一块long大小的内存
long *l = malloc(sizeof(long));
// 返回
return l;
}
void test03()
{
// 问题: ll 能不能使用? 程序有有什么问题?
long *ll = NULL;
ll = create_long();
free(ll);
}
// 给 long 分配内存,并且赋值为 666, 打印输出
int main()
{
test02();
system("pause");
return EXIT_SUCCESS;
}
9.6内存操作
代码区:不关心.
全局区:项目中所有的文件共享。变量和函数。
extern 类型 变量名;
返回值类型 变量名(参数…);
静态区: 只要函数、变量加上 static 关键字,这些变量和函数只能在当前文件内访问。
字符串常量区: 双引号括起来的字符串。不能修改。
栈区:
-
系统自动管理,不能使用 free 去释放栈区内存。
-
栈区比较小。如果大数据不要放在栈上。当程序运行的时候,栈区大小是固定的。
1. 栈空间占用如果超过最大上限,会出现 Stack Overflow 栈溢出。
-
栈区内存由系统管理,它的内存申请、释放效率非常高的。
int arr1[100000000]; int arr2[100000000]; int arr3[100000000]; int arr4[100000000];
堆区:
- 堆区的内存由开发人员自己申请,自己释放。如果忘记 free, 会出现内存泄露。
- 堆区的内存比较大。真正开发环境下,大量的数据需要放在堆区存储。
- 有些数据,我们需要控制它的生命周期,将数据存储在堆上。
- 堆区相对于栈区,内存管理效率就很低。在项目中,进行优化。内存池。
- 程序一运行,一次性 malloc 一大块内存。
- 当程序需要内存时,找到自己的内存池,使用。
- 用完之后再放到内存池中。
- 减少 malloc free 的次数。
9.7. 内存操作
操作内存时,不用再区分堆、栈等区别。定义变量的时候,需要区分。
memset : 初始化内存
memcpy: 内存拷贝, 不能出现内存重叠。将内存中的字节拷贝到另外一个内存。处理字符串拷贝,建议还是用 strcpy
memmove: 内存移动. 处理内存重叠现象。效率比 memcpy 低。
memcmp: 内存比较。 strcmp 从开始位置到\0比较。
四个函数都是 mem 开头,#include <memory.h> .
memset例子:
// 1. memset 数组初始化方式、int arr[10] = { 0 }、for 循环、memset
void test01()
{
// 数组初始化方式一
// int arr[10] = {0};
// 数组初始化方式二
//int arr[10];
//for (int i = 0; i < 10; ++i)
//{
// arr[i] = 0;
//}
int arr[10];
// 第一个参数,是初始化内存的首地址, 类型 void *
// 第二个参数,将内存初始化成什么值,类型 int
// 第三个参数,从首地址开始多少个字节,设置为 0
memset(arr, 0, sizeof(arr));
for (int i = 0; i < 10; ++i)
{
printf("%d ", arr[i]);
}
printf("\n");
}
// 将int类型设置为0值
void test02()
{
int a = 412312;
memset(&a, 0, sizeof(int));
printf("a = %d\n", a);
}
void test03()
{
// malloc 返回的指针类型,就是指向首元素类型的指针
char *s = malloc(sizeof(char) * 32);
// 将内存初始化成0
memset(s, 0, 32);
printf("s = %s\n", s);
}
内存拷贝:
// memcpy 不能有内存重叠,如果有内存重叠,不保证一定成功。
// memmove 内存实现交换两个变量的值
void test04()
{
// 内存拷贝
int a = 10;
int b = 20;
// 第一个参数: 目标空间的首地址
// 第二个参数: 源空间的首地址
// 第三个参数: 从源空间拷贝多少字节的数据到目标空间
printf("a = %d, b = %d\n", a, b);
memcpy(&a, &b, 4);
printf("a = %d, b = %d\n", a, b);
}
void test05()
{
int arr[] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < 5; ++i)
{
printf("%d ", arr[i]);
}
printf("\n");
// 第一个参数: 目标空间的首地址
// 第二个参数: 源空间的首地址
// 第三个参数: 从源空间拷贝多少字节的数据到目标空间
memmove(arr, arr + 1, sizeof(int) * 4);
for (int i = 0; i < 5; ++i)
{
printf("%d ", arr[i]);
}
}
// 使用 memcpy 交换两个变量的值
void test06()
{
int a = 10;
int b = 20;
printf("a = %d, b = %d\n", a, b);
int temp = 0;
// 将 a 的数据拷贝到 temp 中
memcpy(&temp, &a, 4);
// 将 b 的数据拷贝到 a 中
memcpy(&a, &b, 4);
// 将 temp 的数据拷贝到 b 中
memcpy(&b, &temp, 4);
printf("a = %d, b = %d\n", a, b);
}
内存比较:
// memcmp 内存实现比较两个数组是否相等
// 主要用于判断是否相等, 逐个字节比较
void test07()
{
int a = 10;
int b = 20;
// 第一个参数,参与比较的数据的首地址
// 第二个参数, 参与比较数据的首地址
// 第三个参数,从首地址开始比较的字节数
if (memcmp(&a, &b, 4) == 0)
{
printf("相等!\n");
}
else
{
printf("不相等!\n");
}
}
9.8. 结构体
结构体定义语法:
先定义类型,再使用类型定义变量.
// 类型定义
struct Person
{
int age;
double salary;
char name[64];
};
// 3. 结构体是一个类型, 类型都拿来定义变量
void test01()
{
struct Person p; // 使用 Person 类型定义出变量叫做 p
p.age = 30;
p.salary = 9999.99;
strcpy(p.name, "Obama");
printf("Name:%s Age:%d Salary%lf\n", p.name, p.age, p.salary);
}
定义类型之后,顺便定义全局变量:
// 2. 员工类型 emp, 员工编号 员工姓名 员工工资 员工电话
struct Emp
{
int emp_no; // 员工编号
char emp_name[64]; // 员工姓名
double emp_salary; // 员工工资
char emp_tele[128]; // 员工电话
}my_emp = {10001, "司马狗剩", 6789.98, "1234567890"}; // 定义类型之后,马上定义全局变量 my_emp
void test03()
{
my_emp.emp_no = 10001;
printf("%d\n", my_emp.emp_no);
}