声明、定义、初始化:
声明:使用的时候,在前面加上 extern, 表示变量或函数在其他文件中定义:
extern int a; //前面加上extern
extern int func(int a, double b);
extern 可置于变量或者函数前,以表示变量或者函数的定义在别的文件中,
提示编译器遇到此变量或函数时,在其它模块中寻找其定义。
//注意:函数在声明的时候,extern 可以省略
定义:只要没有加 extern 的声明,都属于定义,有存储空间,可以获取地址,但是地址内没有值
int a;
int a[10];
赋值:对已经定义的变量进行赋值操作
a = 10;
a[10] = { 0 };
初始化:定义变量的同时进行赋值操作,地址和值都可以被获取
int a = 3;
int a[10] = { 0 };
一级指针
1. 指针的长度:32位系统下是4个字节,64位系统下是8个字节
1)在定义的时候使用*号,代表这个变量是指针类型
int a = 10; //定义一个整型变量 a, 并赋值 10
int *p = &a; //定义一个整型指针变量 p,并赋值 a 的地址
2)在使用时候使用*号,代表取值运算符,可以取出这个地址里的值
printf("%d\n", *p = *p + 1); // 通过指针 p 进行自增1,值为 11
printf("%d\n", a); // 同时原值 a 的值也变成 11
printf("%d\n", a = a + 1); // 对 a 进行自增1, a的值为 12
2. 几种指针特殊的定义方式:
1) int *const p;
指针常量:指针本身是常量,它指向的地址不可修改,也就是不能再指向别的地方,但是可以修改它所指向的地址里的内容。
举例:
int a = 10;
int b = 20;
int *const p = &a;
// p = &b; //错误
*p = 100; //允许
2) const int *p;
int const *p; //和const int *p等价
常量指针:指向一个常量的指针,它所指向的地址里的内容不可修改,但是可以修改这个指针指向的地址。
举例:
int a = 10;
int b = 20;
const int *p = &a;
p = &b; //允许
// *p = 100; //错误
3) const int *const p;
常量指针常量:指向常量的指针常量,它所指向的地址不可修改,并且地址的内容也不可修改。
例子:
int a = 10;
int b = 20;
const int *const p = &a;
// p = &b; //错误
// *p = 100; //错误
切记,如果你定义了一个指针,那么就一定要知道这个指针指向了什么地方。
其实C语言之所以强大,其自由性和执行效率,很大部分体现在其灵活的指针运用上。
“自由的代价,是永远的警惕。” ———— 《C Primer Plus》
//
多级指针
#include <stdio.h>
int main()
{
int a = 10;
int* p = &a; // 一级指针存储普通变量a的地址;
int** pp = &p; // 二级指针存储一级指针变量p的地址
int*** ppp = &pp; // 三级指针存储二级指针变量pp的地址
// 在定义变量的时候,* 的意思是定义变量的类型
// 在配合变量使用的时候,* 的意思是取值运算符
printf("%p, %p, %p, %p.\n", &a, &p, &pp, &ppp);
//取出各个指针变量自身的地址
printf("%p, %p, %p, %p.\n", &a, p, pp, ppp);
//%p 打印地址,后面的打印 每个指针变量里存储的值,
// 每个指针变量的值,都是其上一级指针的地址
printf("%p, %p, %p, %p.\n", &a, p, *pp, **ppp);
// %p 用内存地址的形式(即十六进制数),来打印变量的值
//1.利用取地址符&,打印整型变量 a 的地址;
//2.打印一级指针 p 的值,即整型变量 a 的地址;
//3.取出二级指针 pp 的值,即一级指针 p 的地址,再打印一级指针 p 的值,即整型变量 a 的地址
//4.取出三级指针 ppp 的值,即二级指针 pp 的地址,再取出二级指针 pp 的值,即一级指针 p 的地址,
// 通过%p,即整型变量 a 的地址
printf("%d, %d, %d, %d.\n", a, *p, **pp, ***ppp);
// 如果要得出变量 a 的值,那么用了多少级指针,取值时就用多少个取址运算符*
//1.直接打印整型变量 a 的值;
//2.一级指针变量p 指向 变量 a 的地址,通过取值得出 a 的值;
//3.二级指针变量pp 指向 一级指针变量p 的地址,取出p的地址后,再通过这个地址进行取值,得出 a 的值;
//4.三级指针变量ppp 指向 二级指针变量pp 的地址,取出pp的地址后,
//再通过这个地址取出一级指针变量p的地址,再通过得出的地址进行取值,最后得出 a 的值
return 0;
}
//
数组指针和指针数组
1. 数组指针(行指针):
定义形式: int (*p)[n];
()和[]优先级相同,但是结合性是从左往右,所以*和p先结合,说明这是一个指针
这个指针指向一个整型的数组,这个数组每行的长度是n,也就是p的步长。
int main()
{
int a[2][3] = {10, 20, 30, 40, 50, 60};
int (*p)[3] = a;
printf("%d\n", *(*p + 1));
// 这个语句执行后,也就是取出行首元素的下标,往后移动一位,再取值,结果为20
printf("%d\n", *(*(p + 1)));
// 这个语句执行后,也就是先把下标后移3个int类型的大小,即跨过了一行的长度(p的步长),然后再取值,结果为40
return 0;
}
2. 指针数组:
定义形式: int* p[n] = { 0 };
[]的优先级要高于*, 那么p先和[n]先结合,代表这是一个数组,再和 int* 结合,代表里面的数组元素都是指针。
比如说,表示某一个元素的方式:p[0],p[1]...p[n];每个元素都可以用来存放其他地址。
int main()
{
int a[3] = {10, 20, 30};
int *p[3];
for (int i = 0; i < 3; ++i)
p[i] = &a[i];
for (int i = 0; i < 3; ++i)
printf("%p\n", p[i]);
return 0;
}
总结:
1. 数组指针:就是一个指针变量,在C语言里面用来指向一个二维数组的首地址,
在内存空间中占有一个指针大小的存储空间,注意这个指针是一个常量。
2. 指针数组:就是一个数组,这个数组每个元素都是一个指针,
他在内存空间中占用了n个指针大小的存储空间。
一维数组 和 指针
int num[5] = {10, 20, 30, 40, 50};
int *p = num;
打印的值 打印后*p的值 源地址的值(源值)
// 操作地址
*p++ 10 20 10
*(p++)
*和++的优先级相同,根据结合性,从右往左,那么p先和后自增运算符++相结合,
++操作将在表达式完成后进行自增,也就是取出p的值之后,p的下标后移一位。
*++p 20 20 10
*(++p)
*和++的优先级相同,根据结合性,从右往左,所以p先和前自增运算符++相结合,
++操作将在表达式完成前进行自增,也就是先将p下标后移一位,再打印p的值。
//操作值
(*p)++ 10 11 11
根据优先级,p先和*相结合,然后再和后自增运算符++结合,
因为是后自增,所以先打印当前下标的值,在源值基础上进行自增,这时候会改变源值。
++*p 11 11 11
++(*p)
根据优先级,p先和*相结合,然后再和前自增运算符++结合,
因为是前自增,所以先在源值的基础上进行自增,然后在打印当前下标的值,这时候会改变源值。
总结: 如果一个表达式中有多个运算符,则先进行优先级比较,先执行优先级高的,
如果优先级相同,那么就看结合性,根据结合性的方向来做运算。
结合性:
从左往右:简称左结合,变量名或者表达式 在运算符两侧,运算顺序是 从左往右的
(括号运算符、./->成员选择、双目运算符,逗号运算符)
从右往左:简称右结合,变量名或者表达式 在运算符两侧,运算顺序是 从右往左的
(单目运算符、三目运算符、赋值类运算符)
前自增自减运算符: ++a; --a;
++a对应的是: addl $0x1, 0x2f(%esp) // 基于当前地址加1 ($代表当前所在地址,%是取寄存器的值)
直接从变量a所在的内存地址中取值,并进行加1操作,再执行之后的语句。
后自增自减运算符: a++; a--;
a++对应的是: mov 0x2f(%esp), %eax // 先把寄存器esp的值移动到寄存器eax里(% 是取寄存器的值)
addl $0x1, 0x2f(%esp) // 基于当前地址加1,表达式执行完后,再把参数入栈
编译器会重新弄个寄存器来保存当前a的值,然后再对a操作,取值时是从缓冲区取,而不是直接从a的内存地址里取。
寄存器 > L1/L2/L3缓存(Cache) > RAM内存
CPU只和寄存器进行数据存取,寄存器用来暂时存储数据,对于重复操作的数据,会暂时放在缓存里。
不管是寄存器和缓存,他们的数据都是来自于内存,而内存又分为四个区...
四、二维数组和指针;
#include <stdio.h>
int main()
{
int p[2][3] = {{10, 20, 30}, {40, 50, 60}};
printf("%d\n", p[1][2]); // 求出第1行第2列的元素:60
printf("%d\n", **p); // 相当于p[0][0]: 10
printf("%d\n", *(p[1] + 2)); // 先取出p[1]的下标,再后移两个int大小的长度,再取值: 60
printf("%d\n", (*p + 1)[2]); // 先取出p的首地址下标,再后移一行,
// 再以这一行为行首,取出这一行的第2个元素,40:
printf("%d\n", *((*p + 1) + 2)); //这个表达式相当于*(*p + 3),
// 把这一行后移一位,再后移2位,也就是行下标偏移了3位,再取出这个行下标里的值:40
printf("%d\n", *(*p + 1) + 2); // 取出数组首地址行下标后移一位,再取出这个下标的值,再加2:22
return 0;
}
总结: int **p; 是一个整型的二级指针,p是可变的句柄,我可以让它指向任何我们想让它指向的位置,
但是这个句柄没有内存空间,所以也不需要指定大小。
int p[n][m] = { 0 }; 是一块内存空间,p是引用了这块空间的句柄,我们可以通过这个句柄找到这个二维数组在内存中的位置
这块空间的大小是 sizeof(int) * n * m,这个p其实是一个不可变的常量,只能指向这块空间。
五、函数参数的 进栈顺序 和 参数计算顺序;
1. 函数参数的进栈顺序
#include <stdio.h>
void func(int x, int y, int z)
{
printf("x = %d : [%p]\n", x, &x);
printf("y = %d : [%p]\n", y, &y);
printf("z = %d : [%p]\n", z, &z);
}
int main(void)
{
func(100, 200, 300);
return 0;
}
// Win32下的编译结果
x = 100 : [0x0018F6d0] +4 //x
y = 200 : [0x0018F6d4] +4
z = 300 : [0x0018F6d8]
// Mac OS 编译后的结果
x = 100 : [0x7fff560f02c8] -4 //x
y = 200 : [0x7fff560f02c4] -4
z = 300 : [0x7fff560f02c0]
C程序栈底为高地址,栈顶为低地址,上面的例子看的出来:
在VC++编译器下,参数的进栈顺序是 "从右往左"的。
但是在UNIX/Linux编译器下,参数的进栈顺序是并不是从左往右的,如果通过汇编代码,我们发现在参数入栈时顺序的确是从右向左入栈,但是在入栈前先把参数列表里的表达式算一遍得到表达式的结果,最后再把这些运算结果统一入栈。
2.函数参数的计算顺序
#include <stdio.h>
int main(void)
{
int a = 10, b = 20, c = 30;
printf("%d..%d..%d \n", a + b + c, (b = b * 2), (c = c * 2));
return 0;
}
//Win32下的编译结果
110..40..60
//Mac OS下的编译结果
60..40..60
由此可知:
在UNIX/Linux编译器下,参数的计算顺序是 "从左往右"
在VC++编译器下,参数的计算顺序是 "从右往左"
首先要确定一点:函数的参数进栈的顺序必然是从右向左的;
但是, 函数的参数 "进栈顺序" 和 "参数计算顺序" 不是一个概念。
所以,当一个函数带有多个参数时,C/C++语言没有规定函数调用时实参的求值顺序,而是编译器自己规定的。
3. 函数的默认参数
#include <stdio.h>
void func(int x, int y, int z = 300)
{
printf("x = %d at [%p]\n", x, &x);
printf("y = %d at [%p]\n", y, &y);
printf("z = %d at [%p]\n", z, &z);
}
int main(void)
{
func(100, 200);
return 0;
}
上面的写法,在某些编译器中是可以通过的,并且 z 的值是300,func(100, 200)的值给了x 和 y;
但是在VC++中不允许这么做,也不允许进行函数参数列表赋值,所以我们在进行函数传参的时候,一定要匹配函数参数列表。
五、内存四区
stack: 栈区,是由编译器自动分配和释放,主要是存放函数的参数的值、局部变量的值。先进后出,后进先出。
heap: 堆区,是由程序员自己申请分配和释放,需要用到 malloc(); calloc(); realloc() 函数来申请创建
用 free() 来释放;如果不释放,程序结束后系统会自动回收。 先进先出,后进后出。
//函数不能通过返回指向栈内存的指针,但是可以返回指向堆内存的指针。
data: 数据区: 主要包括了 静态(全局)区 和 常量区:
全局区(静态区,标有 static 的) : 全局变量、静态变量 存放在这里;
1. 初始化全局变量和初始化静态变量,在一块区域;
2. 未初始化的全局变量和未初始化的静态变量,在一块区域。
统一由程序结束后系统回收。
常量区:这里的数据是只读的,常量和字符串就保存在这里,统一由程序结束后系统回收。
从块的角度来看,这个区可以分为很多种不同的区域。
code:代码区, 用于存放编译后的可执行代码,也就是机器码。
static 关键字详解:
static 变量:
1. 局部静态变量: 就在函数中定义,生命周期是整个源程序,但是作用域和自动变量没区别,
都是只能在定义该变量的函数范围内使用,退出该函数后,尽管这个变量还在,但是已经不能使用。
2. 全局静态变量: 全局变量本身就是静态存储的,静态全局变量也是静态存储方式,但是有区别:
a. 非静态全局变量:变量的作用域是整个源程序,但是其他源文件也可以使用,
b. 静态全局变量:会限制作用域范围,只能在定义该变量的源文件内有效,在其他源文件内不能使用。
static 函数(也叫 内部函数):
只能被本文件中的函数调用,不能被同意源程序的其他文件中的函数调用,
区别一般的非静态函数(外部函数),
static 在C语言里既可以修饰变量,也可以修饰函数。
总结: 1. 如果全局变量只允许在当前文件中访问,就可以将这个全局变量修饰为"静态全局变量"。
2. 如果某个局部变量仅在单个函数中使用,就可以将这个变量修饰为"静态局部变量"。
3. 全局变量、静态局部变量、静态全局变量,都存放在数据区里。
4. 什么时候必须要用到 static 变量:
当某个函数的返回值是一个局部变量的地址,就必须使用 static ;
如果返回的是一个 auto 变量,虽然地址返回了,但是变量的值已经木有了。s
六、动态内存分配——堆的申请:
( 记得 free();)
1 malloc(n * sizeof(int))
请求n个连续的、每个长度是一个int大小的空间,如果创建成功,返回这些空间的地址;如果创建失败,返回 0;
2 calloc(n, sizeof(int))
请求n个连续的、每个长度是一个int大小的空间,如果创建成功,返回这些空间的地址;如果创建失败,返回 0.
和 malloc() 的区别在于, calloc() 会自动把空间初始化为 0;
3 realloc(p, sizeof(int) * n)
给一个已经分配了地址的指针p重新分配空间,参数p 是原来的空间首地址,sizeof(int) * n 是重新申请地址长度
主要用于原来的空间大小的不够啦,要重新分配。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int a = 10; // 全局 初始化区域
char *p1; // 全局 未初始化区域
char *pp;
char* funcA()
{
char *pa = "1234"; //pa 在栈区,"1234" 在常量区,如果函数funcA()结束后,pa会被回收
char *pb = NULL; // pb 在栈区,pb 在栈区分配了4个字节
pb = (char*)malloc(100); //在funcA()函数内申请了一块堆区的内存空间,并把这个空间的首地址赋给了pb
strcpy(pb, "hahaha!");
pp = pb;
//pb = p;
return pb; //返回pb给主调函数
}
char* funcB()
{
char *pa = NULL; // pa 在栈区内分配了4个字节
pa = funcA(); // 把funcA()返回的地址给了pa
printf("%s\n", pa);
return pa; // 返回pa给主调函数 main()
}
int main()
{
int b; // 栈区
char arr[] = "hello"; //栈区
char *p2; //栈区
char *p3 = "world!"; //p3 是在栈区, "world!\0" 在常量区
static int c = 0; // 静态初始化区域
p1 = (char*)malloc(20); // 申请的空间在堆区
p2 = (char*)malloc(20); // 申请的空间在堆区
memset(p1, 0, sizeof(char) * 20); // 清空为 0
memset(p2, 0, sizeof(char) * 20); // 清空为 0
strcpy(p1, "areyouok?"); //"areyouok?" 在常量区,
//printf("%p\n", funcB()); // 打印funcB()返回的地址
//printf("%s\n", p);
free(p1); // 释放p1的空间
free(p2); // 释放p2的空间
char *p; // 创建一个局部指针变量 p
p = funcB(); // p 接收了funcB()的返回值,也就是funcA()申请的堆空间的地址
printf("%s\n", p); // 在还没有释放堆的时候,还是可以打印出字符串的
free(p); // 通过 p 释放了原来的堆空间,
printf("%s\n", p); // 释放后无法再打印字符串了
printf("%s\n", pp); // pp 也无法再找到原来的堆空间了
return 0;
}
论空间分配速度:
栈区速度要快于堆区。
使用栈时,是直接从地址读取数据到寄存器,然后放到目标地址;
使用堆时,第一步将分配的地址放到寄存器,然后取出这个地址的值,然后放到目标地址。大概是这样,堆的数据读出要多一
论空间访问速度:
对于CPU来说是一样的,都是一个直接寻址过程。
七、结构体
在C语言中,结构体所占的内存是连续的,但是各个成员不一定是连续的;
根据结构体定义的成员顺序,引出了一个概念:"字节对齐"
1. 结构体变量的大小,一定能够被其最大类型成员的大小整除,也就是最大那个成员的整数倍
2. 结构体每个成员相对于结构体首地址的偏移量,每个成员直接会有填充字节,以保证字节对齐
#include <stdio.h>
struct Box
{
char type; // 盒子的类型
double wigth; // 盒子的宽
int height; // 盒子的高
};
int main()
{
struct Box box; // 定义了一个Box类型的结构体变量
box.height = 10;
box.wigth = 3.3;
box.type = 'C';
printf("box = %p\n", &box);
printf("box.type[%p] = %c\n", &box.type, box.type);
printf("box.wigth[%p] = %f\n", &box.wigth, box.wigth);
printf("box.height[%p] = %d\n", &box.height, box.height);
printf("size[box] = %d\n", sizeof(box));
return 0;
}
总结:
1. struct 类型变量box 和 struct 第一个成员的首地址相同;
2. sizeof(box) 来获取struct Box 变量的总大小,但是不一定是各个成员之间大小的总和;
3. 不要假设某个结构体成员相对于另一个成员之间有多少个内存便宜了,因为结构体成员之间有边界字节,
编译器可能会把他们放在不连续的内存空间里,因为C标准没有明确规定一个结构体成员的内存必须是连续的
之规定了,先声明的成员必须具有较低的内存地址
比如说例子里的: type 的地址一定低于 wigth
// 初识链表
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct Student
{
char* name; // 姓名
int age; // 年龄
struct Student* next; // 结构体指针成员 next,用来指向某个结构体变量,以便连接起来
} stu, *stu1; //结构体变量 stu 和结构体指针变量 stu1
int main()
{
stu.name = (char*)malloc(10 * sizeof(char));
//结构体变量stu成员name 分配了10个字节
strcpy(stu.name, "damao");
// 拷贝字符串给 stu.name;
stu.age = 18;
// 给 stu.age 赋值
stu1 = (struct Student*)malloc(sizeof(struct Student));
// 为结构体指针变量stu1分配了一个结构体大小的堆空间
stu1->name = (char*)malloc(10 * sizeof(char));
// 给结构体指针变量stu1成员name分配了19个字节的堆空间
stu.next = stu1;
// 把stu1的地址赋给stu的成员next结构体指针, 这样链表就成立了!
strcpy(stu1->name, "ermao");
// 拷贝字符串给stu1的成员name
stu1->age = 16;
// 给stu1的成员age 赋值
stu1->next = NULL;
// 将stu1的next ,也就是指向的下一个结点,置为空
printf("stu.name: %s, age: %d\n", stu.name, stu.age);
printf("stu1->name: %s, age: %d\n", stu1->name, stu1->age);
// 打印两个同学的信息
free(stu.name);
free(stu1->name);
free(stu1);
// 释放申请的堆空间
return 0;
}
总结:
什么是链表?https://www.baidu.com/
八、文件操作: FILE
1. fopen() 函数: 打开一个文件
#include <stdio.h>
FILE* fopen(const char* path, const char* mode);
参数:
path: 需要打开的文件路径
mode: 文件的打开方式,如下所示
r 以只读的方式打开文件,前提是该文件必须存在(默认是文本文件t)
r+ 以可读可写的方式打开文件,该文件必须存在
rb+ 以可读可写的方式打开一个二进制文件
rt+ 以可读可写的方式打开一个文本文件
w 打开只写文件,如果文件存在则内容清空;如果文件不存在则建立这个文件
w+ 打开可读可写的文件,如果文件存在则内容清空;如果文件不存在则建立这个文件
wb 以只写的方式打开或新建一个二进制文件
wb+ 。。。。
wt 以只写的方式打开或新建一个文本文件
wt+ 。。。。
a 以追加的方式打开一个只写文件,如果文件存在,则写入的数据直接追加到内容尾部;
如果文件不存在,则建立该文件
a+ 以追加的方式打开一个可读可写文件,如果文件存在,则写入的数据直接追加到内容尾部;
如果文件不存在,则建立该文件
ab 以追加的方式打开一个二进制文件,如果文件存在,则写入的数据直接追加到内容尾部;
如果文件不存在,则建立该文件
ab+
at 以追加的方式打开一个文本文件,如果文件存在,则写入的数据直接追加到内容尾部;
如果文件不存在,则建立该文件
at+
总结:文件打开模式由 r. w. +. a. b. t
r(read) : 读
w(write) : 写
+(plus) : 读或者写,配合r、w、a
a(append) : 追加
b(binary) : 二进制文件
t(text) : 文本文件
返回值:
如果文件顺利打开,则返回值是指向这个文件流的文件指针
如果文件打开失败,返回 NULL,
一般来说,打开文件后会做一些文件读取湖综合写入的动作,如果文件打开失败后面读写都无法进行
所以,一般都会在fopen()后面做一个错误判断
if(NULL == fp)
C语言知识点超级总结
最新推荐文章于 2022-04-02 16:53:02 发布