c语言学习7

函数传参
1、函数中定义的变量属于该函数,出了该函数就不能再被别的函数直接使用
2、实参与形参之间是以赋值的方式进行传递数据的,并且是单向值传递
3、return语句其实是把返回值数据放入公共区域内存中(调用者和被调用者都可以访问),调用者会从该区域获取返回值;如果不写return语句,该区域会是一个随机的垃圾数据,调用者也能拿到返回值但是无意义。
4、数组作为函数的参数传递时,数组的长度会丢失,需要额外增加一个变量把数组的长度传递过去
void func(int arr[],int len);
int arr[10];
func(arr,10)
5、数组作为参数传递时,是"址传递",相当于调用者与函数共享数组
练习1:实现一个函数,找出数组中的最大值
练习2:实现一个函数,对数组进行排序
练习3:实现一个函数,查找数组中是否存在某个值,如果存在返回该数值在数组中的下标

设计函数的准则:
1、一般一个函数最好不要超过50行,确保一个函数只负责完成一项功能,降低出错概率,提高可读性
2、数据一般要由调用者提供,只把结果返回给调用者,确保函数的通用性
3、考虑调用者提供的非法数据,可以先判断后使用,也可以通过注释或说明来写明情况,提高函数的健壮性

进程映像:
程序:存储在磁盘上的可执行文件(二进制文件、脚本文件)
进程:正在系统中运行的程序
进程映像:进程的内存分布情况 //注意:在内存中,从地址到高地址依次是:text、data、bss、heap、stack(其他映像都是从低地址往高地址走,而stack是从高地址往低地址走的,
//这样的原因是:1、空间利用率:设计使得堆和栈可以动态地使用剩余的内存
//2、防止冲突:防止相互覆盖溢出
//3、安全性:栈的增长方向通常是向下的,这是因为大多数系统中的栈是用来存储函数调用和局部变量的,这些数据的生命周期短且易变。
//如果栈向上增长,那么栈溢出可能会覆盖到静态数据或者程序代码,这会带来安全性问题。)
text 代码段:(代码段+只读段)
存储的是二进制指令、常量,权限是只读,如果强制修改会产生段错误
data 数据段:
初始化的全局变量、初始化过的静态局部变量
bss 静态数据段:
未初始化的全局变量、未初始化的静态局部变量(//需要注意的是这两种变量初始化的值为0时,也是存储在bss段中的)
在该段内存中的数据在程序开始前会自动清理为0
stack 栈:
局部变量和块变量,会随着程序的运行不断地申请、释放,由操作系统管理,使用方便,内存小
heap 堆:
该段内存由程序员手动管理,使用麻烦,足够大

局部变量和全局变量:
全局变量:定义在函数外的变量
存储位置:data(初始化) 或者 bss(未初始化)
生命周期:程序开始到程序结束
使用范围:程序的任意位置都可以使用
局部变量:定义在函数内的变量
存储位置:stack 栈内存
生命周期:从函数开始到函数结束
使用范围:只能在该函数内使用
块变量:定义在if/for/while等语句块内的变量
存储位置:stack 栈内存
生命周期:从语句块开始到语句块结束
使用范围:只能在语句内使用

注意:同名的局部变量会屏蔽同名的全局变量
    同名的块变量会屏蔽同名的全局、局部变量
    因此建议全局变量首字母大写,局部变量全部小写

类型限定符:
auto
用于定义自动申请、自动释放的变量(局部变量),不加就代表加了
注意:在C11语法标准中用于自动类型识别
auto num = 10; //int
auto num = 3.13;//double
注意:不用用它修饰全局变量
extern
用于声明外部变量,意思是告诉编译器此变量在程序的其他地方已经定义了,先让程序通过编译,如果在链接时找不到该变量依然会报错
不建议在extern时赋值,它只是声明

static
   改变存储位置:
        改变局部变量的存储位置,由stack改为data(初始化)或者bss(未初始化)
   延长生命周期:
        延长局部变量的生命周期,直到程序结束才释放
   限制作用范围: 
        限制全局变量、函数的使用范围,限制只能在本文件内使用
        注意:使用static修饰全局变量,可以防止该变量被别的文件使用,以及防止命名冲突

const
    "保护"变量的值不被显式地修改
    注意:如果通过内存进行修改,还是可以改的
    注意:使用const修饰data段数据,那么该数据会存储到text段中,如果强制修改会段错误

volatile
    C编译器会对普通变量的取值进行"取值优化",只要在使用变量过程中该变量没有显式改变,那么编译器会直接使用上一次的结果,而不会每次都去内存读取数据
    加上volatile修饰,让编译器不要对该变量进行"取值优化"
    一般在驱动编程、硬件编程、多线程编程时使用
    volatile int num = 10;
    if(num == num)
    {
        //  可能为假
    }

register
    存储介质:
        硬盘->内存->高级缓存->寄存器->CPU
    申请把变量的存储介质由内存改为寄存器,但是由于寄存器数量有限,不一定百分百成功
    注意:寄存器变量不能取地址

typedef
    类型重定义
    在定义变量前,加上typedef,那么原本的变量名就变成了这种数据类型,可以像数据类型一样定义变量
    typedef int num;
    num n1;

讨论:关于typedef和宏定义的区别
    例:
        typedef int INT
        INT n1;
        #define INT int 
        INT n1; //以上这两个其实在使用时是效果一样的,一个类型重定义,一个是宏替换。

    例:
        typedef int* INTP 
        INTP p1,p2,p3;
        define INTP int*
        INT p1,p2,p3;这次两个是有区别的,因为上面的那个还是定义了三个指针,但是第二个变成了int *p1,p2,p3;这样只是定义了一个p1指针,p2和p3是整型
        一、什么是指针
指针是一种特殊的数据类型,使用它可以定义指针变量,指针变量中存储的是整型数据,该数据代表了内存的编号(地址),可以通过这个编号访问到对应的内存

二、为什么要使用指针
1、函数之间内存是相互独立的,但有时候需要函数之间共享变量
普通传参是单向值传递
全局变量容易命名冲突
使用数组还需要额外传递长度
虽然函数之间内存空间和命名空间是相互独立的,但是地址空间是同一个,所以使用指针可以解决这个问题

2、由于函数之间普通变量是单向值传递(拷贝),因此对于一些字节数比较多的变量,值传递的效率很低,如果传递的是地址只需要4(32位)\8(64位)字节,可以提高传参效率

3、堆内存无法取名字,它不像data、bss、stack这些可以让变量名与对应的内存建立联系,只能使用指针变量记录堆内存的地址从而使用堆内存

三、如何使用指针
定义:
类型名* 变量名_p;
1、指针变量与普通变量的用法有很大区别,建议在取名时以p结尾加以区分
2、指针变量的类型表示它存储的是什么类型变量的地址,它决定了通过该指针变量能够连续访问的字节数
3、一个只能定义一个指针变量
int
a,b,c; // a是指针变量,bc是int类型变量
int p1,p2,*p3; //p1p2p3都是指针变量
4、指针变量与普通变量一样,默认初始值是随机的,一般初始化为NULL

2、赋值
    变量名_p = 地址;    //必须是有权限且有意义的内存地址
        栈内存:    int num;int* p; 
            p = #
        堆内存:
            p = malloc(4);
    
3、解引用
    *变量名_p;
        *p = 10;
        printf("%d",*p);
    通过该指针变量中存储的内存编号去访问对应的内存,具体连续访问的字节数由该指针类型决定
    注意:该过程可能会产生段错误,根源是该指针变量中存储的是非法内存地址

四、使用指针需要注意的问题
空指针:值为NULL的指针变量叫做空指针,如果对空指针解引用就会产生段错误
NULL一般用于初始化指针变量
NULL是一种错误标志,如果一个函数的返回值类型是指针类型时,该函数执行出错则可以返回NULL
NULL可以被判断 if(NULL == p) if(!p)
注意:绝大多数系统中NULL就是0,个别系统中是1
如何避免空指针带来的段错误?
当使用来历不明的指针前一定要先做判断
1、当函数的返回值是指针类型,获取后先判断后使用
2、当你函数的参数是指针时,别人可能会传空指针,使用前先判断

野指针:指向不确定的内存空间的指针叫做野指针
    对野指针解引用有什么后果:
        1、一切正常
        2、段错误
        3、脏数据
    野指针比空指针危害更大,因为它无法判断出来,并且它的问题可能是隐藏性的短时间内不暴露而已
    所有的野指针都是程序员自己制造
    如何避免产生野指针:
        1、定义指针变量时一定要初始化
        2、函数不要返回局部变量(栈内存)的地址
        3、当指针所指向的内存被释放后,指针变量要及时置空
            int* p = malloc(4);
            free(p);
            p = NULL;

五、指针的运算
指针变量中存储的是整数,理论上整形数据可以使用的运算符它都可以使用,但是绝大多数运算都无意义
指针 + n <=> 指针+指针类型字节数n 前进了n个元素
指针 - n <=> 指针-指针类型字节数
n 后退了n个元素
运算后得到的结果依然是一个临时的指针
指针 - 指针 <=> (指针-指针)/类型字节数 计算出两个指针变量之间间隔了多少个指针元素个数
必须类型相同的指针才可以相减

六、const与指针
就近原则:看const右边先跟着的是*(内存) 还是 p(指向)

const int* p;   保护指针所指向的内存数据不能修改
int const *p;   同上
int* const p;   保护指针的指向不能修改
const int* const p; 保护指针指向的内存和指向都不能修改
int const * const p; 同上
当我们为了提高传参效率而使用指针时,传参效率提高了,但是变量共享后有被修改的风险,因此配合const可以进行保护

七、指针数组与数组指针
指针数组:
就是由指针组成的数组,它的成员都是类型相同的指针变量
类型* arr[长度] = {};
int* arr[10];
数组指针:
专门指向数组的指针
类型 (arrp)[长度];
int (
arrp)[10];
含义:arrp是一个专门指向类型为int,长度为10的数组的指针
当使用堆内存的二维数组时会使用它俩

八、指针与数组名
数组名是一种特殊的"指针",它与数组在内存中的首地址之间存在映射关系,它没有自己的存储空间,数组名是常量,不能修改它所代表的值
&arr[0] == &arr == arr
指针变量有自己的存储空间,它与内存之间是指向关系,如果它存储了数组的首地址时,那么指针可以当做数组使用,同时数组名也可以当做指针使用
int* p = arr;
p[i] == *(p+i)
arr[i] == *(arr+i)

九、二级指针
二级指针就是指向指针的指针,里面存储的是指针变量的地址
定义:
类型名** 变量名_pp;
赋值:
变量名_pp = &指针变量;
解引用:
*变量名_pp == 指针变量;
**变量名_pp == *指针变量 == 数据

热门相关:有个人爱你很久   富贵不能吟   万古至尊   上神来了   万古至尊