每日英语:
建议全局变量名以g_开头,g = global:全局
static:静态
dynamic:动态
pointer:指针
回顾:
1. 二维数组
-
二维数组本身还是一维数组,做了分组而已,搞成了I行J列
-
定义二维数组的语法:数据类型 数组名[I] [J] = {初始值}
瞬间脑子实现二维数组的内存分布图
-
定义二维数组的形式
-
二维数组公式
a = a[0] = &a[0][0] 各种sizeof
2. 函数
-
函数功能:语句的组合,具有独立性和通用性
-
函数的使用3步骤:
函数声明:extern 返回值 数据类型 / void 函数名(形参表)
函数定义:
返回值数据类型/void 函数名(形参表/void) { 函数体语句; returnreturn 数值/不需要 }
函数调用:变量 = 函数名(实参表/空);
-
函数使用的4种形式:
无参无返回值/有参无返回值/无参有返回值/有参有返回值
记住:swap经典代码案例
-
函数和数组的关系
编程公式:
返回值数据类型/void 函数名(数组首地址(int a[]), 其余形参参数自行添加定义) { a[元素下标]; // 获取元素值 a[元素下标] = 新值: // 修改元素值 } // 此函数可以直接通过首地址a修改访问数组内存!
作业:
编写两个函数实现将数组中某个元素的某个bit位清0或者置1
参考代码:bit.c
/*函数那点事*/
#include <stdio.h>
/*声明打印函数print,清0clear_bit, 置1set_bit */
extern void print(int a[], int index, int len);
extern void clear_bit(int a[], int index, int bit, int len1); // bit 位编号
extern void set_bit(int a[], int index, int bit, int len2);
int main(void) {
int a[2] = {0x55, 0xaa};
int len = sizeof(a) / sizeof(a[0]);
int len1 = sizeof(a) / sizeof(a[0]);
int len2 = sizeof(a) /sizeof(a[0]);
//清0
clear_bit(a, 0, 0, len1); // 调用clear_bit函数
print(a, 0, len);
clear_bit(a , 1, 1, len1);
print(a, 1, len);
print(a, 2 ,len);
//置1
set_bit(a, 0, 0, len2); // 调用set_bit函数
print(a, 0, len);
set_bit(a, 1, 1, len2);
print(a, 1, len);
return 0;
}
/*定义print,clear_bit,set_bit*/
void print(int a[], int index, int len) {
if(index >= len) { // 添加安全保护,防止越界访问
printf("所取元素小标超出范围\n");
}
printf("数组的%d个元素是:%#x\n", index, a[index]);
}
void clear_bit(int a[], int index, int bit, int len1) {
if(index >= len1) {
printf("所取得第%d个元素超出范围\n", index);
}
a[index] &= ~(1 << bit);
}
void set_bit(int a[], int index, int bit, int len2) {
if(index >= len2) {
printf("所取得第%d个元素超出范围\n", index);
}
a[index] |= (1 << bit);
}
结果:
数组的0个元素是:0x54
数组的1个元素是:0xa8
所取元素小标超出范围
数组的2个元素是:0x2
数组的0个元素是:0x55
数组的1个元素是:0xaa
第八课:变量(包括数组)作用域和可见性
1. 明确
- c语言变量按照数据类型(占内存大小)分:12种(char, short,…double)
2. C语言变量按照作用域和可见性分:
局部变量和全局变量
3. 局部变量
定义在函数内部的变量
例如:
void A(void) {
int a = 250;///局部变量
}
注意:局部变量如果不初始化gcc会给一个款七八糟的随机数
4. 全局变量
定义在函数之外的变量
int g_a = 250; 全局变量
void A(void) {
int a = 250; // 局部变量
}
int g_b = 250; //全局变量
注意:全局变量不初始化,gcc一律给0
5. static关键字特性
如果定义变量时,前面加static关键字修饰,此变量又称静态变量
如果定义变量前面,不加static’关键字修饰,此变量又称非静态变量
例如:
static int a = 250; //静态变量
int a = 250;// 非静态变量
6. 终极结论:
C语言变量最终分为4类:
局部非静态变量/局部静态变量/全局非静态变量/全局静态变量
7. 详解局部非静态变量特点:
7.1 形式1
void A(void) {
printf("%d\n", a); // 不可以,gcc报错,变量a没有定义的错误
int a = 250; // 局部非静态变量
printf("%d\n", a);
}
7.2 形式2
viod A(void) {
if(1 == 1) {
int a = 250;
printf("%d\n", a); //可以
}
printf("%d\n", a); // 不可以,gcc报错
}
7.3 函数的形参
void A(int a) { //a在整个函数体内部都可以访问,但是出了函数就不能再访问了
printf("%d\n", a);
}
参考代码:var.c
7.4 切记:局部非静态变量特点
-
此类变量使用范围(访问范围):从定义开始一次向下知道最近的花括号结束
-
此类变量分配的内存生命周期:从定义的地方开始操作系统就会给变量分配内存
直到最近的花括号操作系统立马收回给变量分配的内存
只能的等下一次调用或者运行操作系统继续给变量分配内存
8. 局部静态变量特点(堪称钉子户)
8.1 形式1
void A(void) {
printf("%d\n", a); // 不可以,gcc报错,变量a没有定义的错误
static int a = 250; // 局部非静态变量
printf("%d\n", a);
}
8.2 形式2
viod A(void) {
if(1 == 1) {
static int a = 250;
printf("%d\n", a); //可以
}
printf("%d\n", a); // 不可以,gcc报错
}
参考代码:var.c
/*局部非静态变量演示*/
#include <stdio.h>
/*定义一个A函数*/
void A(void)
{
static int a = 250; // 定义静态变量,钉子户
printf("A函数:a = %d\n", a);
a = 10; // 执行完一次A函数后,它的生命周期不会结束,还在内存中,所以第二次调用A函数就打印10
}
int main(void)
{
// 局部非静态变量
// printf("a = %d\n", a);
int a = 250; //局部非静态变量
printf("a = %d\n", a);
if(520 == a) {
int b = 520;
printf("b = %d\n", b); // 可以
}
// printf("b = %d\n", b); // 不可以 报错
// 局部静态变量演示
A();
A();
return 0;
}
结果:
a = 250
A函数:a = 250
A函数:a = 10
8.3 切记:局部静态变量特点
-
此类变量使用范围(访问范围):从定义开始一次向下知道最近的花括号结束
-
此类变量分配的生存周期:从程序运行时操作系统给变量分配内存,知道程序结束,
操作系统才会收回变量的内存,一次分配,终身使用
9. 详解全局非静态变量特点
9.1 形式1
全局非静态变量的定义和使用访问都是在同一个源文件中进行
例如:
vim var1.c
void B(void)
{
printf("g_a = %d\n", g_a); //不可以访问,gcc会报g_a没有定义的错误
}
int g_a = 250; // 全局非静态变量
void A(void)
{
// 访问使用
printf("g_a = %d\n", g_a); //可以访问
}
参考代码:var1.c
/*全局非静态变量*/
#include <stdio.h>
void C(void)
{
//g_a--; // gcc报错
}
int g_a = 250;
void A(void)
{
printf("A函数:%d\n", g_a); // 正常访问
}
void B(void)
{
g_a++; // 正常访问
}
void D(void)
{
g_a--; // 正常访问
}
int main(void)
{
A();
B();
A();
D();
A();
return 0;
}
结果:
A函数:250
A函数:251
A函数:250
9.2 形式2
全局非静态变量定义和访问使用在不同源文件中进行(又称跨文件访问)
切记:如果一个源文件定义一个全局非静态变量,另一个源文件要想直接访问使用这个全局变量
例如:打印它的值或修改它的值
此时要求访问使用全局变量的文件必须声明这个全局非静态变量
声明全局非静态变量语法:extern 数据类型 全局非静态变量名; // 不要给初值
声明完毕,方可访问定义的全局变量
注意:声明是不会分配内存的
参考代码:var2.c(负责定义全局非静态变量) var3.c(负责访问使用全局非静态变量)
编译命令:gcc -o var2 var2.c var3.c // 将var2.c var3.c编译生成一个可执行文件
运行./var2
var2.c
/*全局非静态变量跨文件访问演示:此文件只负责定义全局非静态变量*/
#include <stdio.h> // 为了使用printf函数
/*定义全局非静态变量*/
int g_a = 250;
/*定义打印函数print*/
void print(void)
{
printf("print函数:g_a = %d\n", g_a);
}
/*定义自增函数*/
void inc_var(void)
{
g_a++;
}
var3.c
/*全局非静态变量跨文件访问演示:此文件只负责访问全局非静态变量*/
#include <stdio.h>
/*声明全局非静态函数*/
extern int g_a;
/*声明打印函数print,自增函数inc_var*/
extern void print(void);
extern void inc_var(void);
/*定义此文件里的自减函数*/
void dec_var(void)
{
g_a--; // 可以访问
}
int main(void)
{
print(); //250
inc_var();
print(); //251
dec_var();
printf("main函数里的自减函数:g_a = %d\n", g_a); //250
return 0;
}
结果:
print函数:g_a = 250
print函数:g_a = 251
main函数里的自减函数:g_a = 250
9.3 切记:全局非静态变量特点
-
此类变量使用范围:分两种情况:
-
如果是在定义变量的源文件中,范围从定义的地方开始一次向下所有的函数都可以访问,前面的函数无权访问
-
如果是在不同文件(跨文件)访问,范围是从声明的地方开始一次向下所有的函数都可以访问
声明之前的函数无权访问
-
-
此类变量分配的内存生命周期:从启动运行程序开始操作系统为其分配内存
知道程序结束,操作系统才会收回变量的内存
-
实际开发建议:全局非静态变量要少用,慎用,能不用最好不用
负责极易出现乱序访问的严重后果
如果非要使用,一定要记得用互斥保护的方法,来保护全局非静态变量
阻止乱序发生,但是这种互斥保护会降低代码的执行效率
注意:互斥机制等第二阶段课程详解
10. 全局静态变量
10.1 形式:
全局静态变量的定义和使用访问,永远只能在同一个文件中进行,不可跨文件访问,
也就是说全局静态变量只能用于本文件,其他源文件访问不了
参考代码:var2.c var3.c
编译:gcc -o var2 var2.c var3.c
运行: ./var2
10.2 切记:全局变量的特点
-
此类变量使用范围:只能用于定义变量所在的文件,不能跨文件访问,并且从定义的地方开始,依次向下所有的函数,都可以访问
-
此类变量分配的内存生命周期:从启动运行程序开始操作系统为其分配内存
直到程序结束,操作系统才会收回变量的内存
-
实际开发建议:全局非静态变量要少用,慎用,能不用最好不用
否则极易出现乱序访问的严重后果,只是它发生乱序的概率,比全局非静态变量要低
如果非要使用,一定要记得用互斥保护的方法,来保护全局非静态变量
阻止乱序发生,但是这种互斥保护会降低代码的执行效率
注意:互斥机制等第二阶段课程详解
11. static终极总结(笔试题必考)
-
static修饰的全局变量只能用于本文件,其他文件不能访问该全局变量
例如:static int g_b = 520;
-
static修饰的函数(在定义函数的时候进行修饰)统同样只能用于本文件的其他函数调用
其他文件不能访问调用此函数
例如:
vim static1.c static int add(int x, int y); { return x + y; } int main(void) { add(10,20); // 可以 return 0; } vim static2.c extern int add(int, int); // 报错 int test(void) { add(100, 200); // 不能调用,报错 }
- static修饰的变量或者函数将来使用起来相对不修饰安全点,某种程度上可以降低乱序发生的概率,多多少少起到了间接保护的作用
参考代码:var2.c var3.c
var2.c
/*全局非静态变量跨文件访问演示:此文件只负责定义全局非静态变量*/
#include <stdio.h> // 为了使用printf函数
/*定义全局非静态变量*/
int g_a = 250;
/*定义全局静态变量*/
static int g_b = 520;
/*定义静态函数:用static修饰的函数*/
static void print1(void)
{
printf("测试static修饰的函数调用关系\n");
}
/*定义打印函数print*/
void print(void)
{
printf("print函数:g_a = %d\n", g_a);
printf("print函数:g_b = %d\n", g_b);
print1(); // 同文件中调用静态函数
}
/*定义自增函数*/
void inc_var(void)
{
g_a++;
g_b++;
}
var3.c
/*全局变量跨文件访问演示:此文件只负责访问全局非静态变量*/
#include <stdio.h>
/*声明全局函数*/
extern int g_a;
// extern int g_b; 报错
/*声明打印函数print,自增函数inc_var*/
extern void print(void);
extern void inc_var(void);
/*声明静态函数*/
// extern void print1(void); // 报错 error: ld returned 1 exit status
/*定义此文件里的自减函数*/
void dec_var(void)
{
g_a--; // 可以访问
}
int main(void)
{
print(); //250
inc_var(); ``
print(); //251
dec_var();
printf("main函数里的自减函数:g_a = %d\n", g_a); //250
// printf("main函数里的自减函数:g_b = %d\n", g_b); 报错
// print1(); // 报错
return 0;
}
结果:
print函数:g_a = 250
print函数:g_b = 520
测试static修饰的函数调用关系
print函数:g_a = 251
print函数:g_b = 521
测试static修饰的函数调用关系
12. 全局变量乱序分析:
例如:
// 定义全局变量
int g_a;
// 自增操作
g_a++;
明确cpu做g_a++数据运算,实际微观上CPU要经过三步骤运算:
第一步:先从内存中将变量g_a的值读取到CPU内部
第二步:然后在CPU内部做加1操作
第三部:最后将CPU内部的运算结果保存到内存中
具体参见:全局变量乱序.png
明确:由于linux系统是一个多任务系统,也就是宏观上可以同时运行多个程序(别名:线程(thread))
程序要想运行必须先获取到CPU,没有CPU无法运行
关键是程序之间还存在优先级,也就是高优先级的程序,抢夺CPU资源的能力
例如:比如A程序正在运行(CPU正在给A用),突然来了个B程序,它的优先级高于A
所以B不带商量的将A的CPU抢夺走,A就停止运行,B开始投入运行,B运行完毕然后将CPU再
归还给A程序,A继续接着运行(不是从头再来一遍)
乱序产生:假设现在有个A程序做以下代码:
for(int i = 0; i < 10000; i++)
g_a++;
// 对g_a加10000次,心里要明白,实际CPU执行这条语句要经过三步骤
第一步:先从内存中将变量g_a的值读取到CPU内部
第二步:然后在CPU内部做加1操作
第三部:最后将CPU内部的运算结果保存到内存中
当A程序假设正在处理第50次加1操作,刚执行完第一步,就在此时此刻高优先级的B程序运行了,B程序毫无条件的抢夺A程序的CPU资源,A就停止第二步和第三步运行
B程序接着运行以下代码
for(int i = 0; i < 10000; i++)
g_a++;
等B执行完1万次加1操作之后,此时B程序将CPU资源归还给A,A继续运行,本来A应该做完第50次加1
而此过程少了一次,此过程如果频繁的发生A和B的切换,将来g_a理论上可以做2万次加1,而实际上肯定少于1万次
参考代码:thread.c
第九课:指针(C语言灵魂)
1. 指针的定义:
指针本质就是一个变量,而这个变量永远只能存储内存地址(编号)
所以此变量对应的专业术语叫指针变量(简称指针)
结论:将来通过指针变量保存的地址就可以对这块内存区域进行任意访问
可以读内存数据还可以修改内存数据
2. 指针变量定义的语法格式
2.1 书写形式1
数据类型 * 指针变量名;
例如:
int * pa; // 定义指针变量pa
2.2 书写形式2
数据类型* 指针变量名;
例如:
int* pa; // 定义指针变量pa
2.3 书写形式3
数据类型 *指针变量名;
例如:
int *pa; //定义指针变量pa
语义:
都是在定义指针变量pa,将来这个变量pa能够保存一块内存区域的首地址
也就是指针变量pa本身也会分配内存,只是它对应的内存用来存储其他内存区域的首地址
此过程简称pa指向某块内存区域
并且指针变量保存的首地址对应的内存区域保存着一个int类型的数字
也就是数据类型不是给指针变量使用,而是给指针变量指向的内存使用的
切记:
指针变量分配的内存空间跟计算机硬件有关
32位系统,一个地址值为32位,4字节,所以对应的指针变量内存大小永远4字节
64位系统,一个地址值为64位,8字节,所以对应的指针变量内存大小永远8字节
所以指针变量没有数据类型一说
例如:
char *pa; //定义一个字符类型的指针变量pa,pa对应的内存位4个字节,而pa指向的内存数据类型位char类型
// 将来它指向的内存只能保存一个char类型的数字,对应的内存也就1字节
short *pa; //定义一个字符类型的指针变量pa,pa对应的内存位4个字节,而pa指向的内存数据类型位short类型
// 将来它指向的内存只能保存一个short类型的数字,对应的内存也就2字节
3. 连续定义多个指针变量形式:
int *pa, *pb; // 定义两个指针变量
int *pa, *pb; // pa是指针变量,pb是普通的变量,不是指针
4. 指针变量初始化方法
通过取地址运算符&进行;
例如:
int a = 250; // 分配4字节内存,存储250数字,而这个数字类型位int类型
int *pa = &a; // 定义一个指针变量分配4字节内存空间,然后保存变量a的首地址
// 简称pa指向a
char b = 'A'; //分配1字节空姐,存储字符A的ASCII码
int *pb = &b; // 定义指针变量分配4字节内存空间,然后保存变量b的首地址,简称pb指向b
注意:指针变量pa和pb指定的数据类型int,char不是给他们用,而是给a和b用的!
此时此刻脑子立马浮现内存操作示意图
问:一旦通过指针变量获取带指定的内存首地址,如何通过指针变量来对指向的内存为所欲为呢(读取值或者修改值)?
答:通过引用运算符*
5. 解应用运算符*
功能:通过指针变量对指向的内存区域进行各种访问操作
语法格式:*指针变量名
例如:
int a = 250;
int *pa = &a;
printf("pa保存变量a的首地址是%p\n", pa);
// 通过pa来获取指向的内存存储的数字250
printf("%d\n", *pa);
printf("pa占的字节%d\n", sizeof(pa));
// 通过pa对指向的内存进行修改
*pa = 520; // 将指向的内存或者变量a的值修改为520
printf("pa保存变量a的首地址是%p\n", pa);
切记切记切记:
sizeof(指针变量名)永远等于4字节
参考代码:pointer.c
/*指针演示*/
#include <stdio.h>
int main(void) {
// 定义初始化字符常量,分配1字节的内存
char a = 'A';
// 定义初始化指针变量pa,pa指向a
char *pa = &a;
// 打印变量a,指针变量pa和指针变量本身的地址
printf("&a = %p, pa = %p, &pa = %p\n", &a, pa, &pa);
// 打印指针变量a和指针变量本身占用内存的代销
printf("sizeof(a) = %d, sizeof(pa) = %d\n", sizeof(a), sizeof(pa));
// 通过pa获取a的值
printf("%hhd, %c\n", *pa, *pa);
printf("%hhd, %c\n", a, a);
// 通过pa修改a的值
*pa = 'B';
printf("%hhd, %c\n", *pa, *pa);
printf("%hhd, %c\n", a, a);
return 0;
}
结果:
&a = 0xbf8a2de7, pa = 0xbf8a2de7, &pa = 0xbf8a2de8
sizeof(a) = 1, sizeof(pa) = 4
65, A
65, A
66, B
66, B
6. 特殊指针:空指针和野指针
6.1 空指针
空指针变量保存一个空地址,用NULL表示,其实就是编号为0的地址
空指针不可随意访问,否则直接造成程序崩溃!
例如:
int *pa = NULL; // pa指向NULL
printf("pa指向的0地址保存的数据是%#x\n", *pa); // 程序直接崩溃
*pa = 250; // 修改0地址数据,程序直接崩溃
注意:全局指针变量没有初始化同样gcc也赋值一个NULL空地址
例如:
int *g_a; // g_a = NULL = 0
6.2 野指针
就是没有初始化的指针变量(局部指针变量),它保存着一个随机地址,胡乱·指向一块没有分配的内存
如果访问野指针,也是非法访问,也会造成程序的崩溃!
例如:
int *pa; //pa就是野指针
*pa = 250; //操作野指针,程序直接崩溃
参考代码:pointer.c
/*空指针*/
int *pb = NULL;
printf("空指针指向的0地址保存的数据是%#x\n", *pb); //崩溃
*pb = 250; //崩溃
结果:
段错误 (核心已转储)