指针到底是什么
指针和变量的区别:
指针的实质就是变量, 指针完整的名字应该叫指针变量, 简称指针
为什么需要指针:
指针是为了实现间接访问在汇编中都有间接访问, 其实就是 CPU 的寻址方式中的间接寻址;
间接访问 (CPU 的间接寻址) 是 CPU 设计时决定的, 这个决定了汇编语言必须能实现间接寻址, 也决定了汇编之上的 C 语言也必须实现间接寻址;
高级语言 C#Java 等并不是没有指针, 而是进行封装了;
指针使用三部曲: 定义指针变量关联指针变量解引用
指针带来的一些符号的理解
星号 *:
C 语言中代表乘号, 也表示指针符号;
星号在用于指针相关功能时有 2 种用法:
第一种: 指针定义时,* 结合前面的类型表明要定义指针的类型;
第二种: 在解引用的时候,*p 表示 p 指向的变量的本身;
取地址符 &:
& 使用时, 直接加在一个变量的前面, 然后取地址符和变量加起来构成一个新的符号, 这个符号表示这个变量的地址;
指针定义并初始化与指针先定义再赋值 的区别
指针定义时, 可以初始化, 指针的初始化其实就是给指针变量赋初值(跟普通变量的初始化没有任何本质区别);
指针变量定义并初始化的格式为: int a =32; int *p =&a;
不初始化时, 指针变量先定义再赋值: int a =32; int *p; p =&a;
左值和右值
赋值操作其实就是: 左值 = 右值;
变量做左值时, 代表的是变量对应的内存空间;
变量做右值时, 代表的是变量空间存的数据;
左值和右值的区别: 就好象现实生活中家这个字的含义譬如我回家了, 这里面的家指的是你家的房子(类似于左值); 但是说家比事业重要, 这时候的家指的是家人(家人就是住在家所对应的那个房子里面的人, 类似于右值)
野指针问题
什么是野指针? 哪里来的? 有什么危害?
野指针就是指针指向的位置是不可知的(随机的不正确的没有明确限制);
野指针很可能触发段错误(Sgmenntation fault);
因为指针变量在定义时如果为初始化, 值也是随机的; 指针变量的值其实就是别的变量 (指针所指向的那个变量) 的地址, 所以意味着这个指针指向了一个地址是不正确的变量, 这时候去解引用就是去访问这个地址不正确的变量, 所以结果是不可知的;
野指针因为指向地址是不可预知的, 所以有 3 中情况:
第一种是不可访问的 (操作系统不允许访问的敏感地址, 譬如内核空间) 的地址, 运行结果是发生段错误, 这种算是最好的情况了;
第二种是指向一个可用的而且没什么特别意义的空间(譬如我们使用过的但是已经不用的栈空间或堆空间), 这时候程序运行不会出错, 也不会对当前程序造成损害, 这种情况不会掩盖你的程序错误, 让你以为程序没问题, 其实是有问题的;
第三种是指向一个可用的空间, 而且这个空间其实在程序中正在被使用(譬如说程序的一个变量 x), 那么野指针的解引用就会刚好修改这个变量 x 的值, 导致这个变量莫名其妙的被改变, 程序出现离奇错误, 一般最终会导致程序崩溃, 或者数据被损害, 这种危害是最大的;
指针变量如果是局部变量, 则分配在栈上, 本身遵从栈的规律(反复使用, 使用完不擦除, 所以是脏的, 本次在栈上分配到的变量的默认值是上次这个栈空间被使用时余留下来的值), 就决定了栈的使用多少会影响这个默认值因此野指针的值是有一定规律不是完全随机, 但是这个值的规律对我们没意义因为不管落在上面野指针 3 种情况的哪一种, 都不是我们想看到的
怎么避免野指针:
野指针的错误来源就是指针定义之后没有初始化, 也没有赋值(总之就是指针没有明确的指向一个可用的内存空间), 然后去解引用;
知道了野指针产生的原因, 避免方法就出来了: 在指针的解引用之前, 一定确保指针指向一个绝对可用的空间;
常规的做法:
第一点: 定义指针时, 同时赋值为 NULL;
第二点: 在解引用之前先判断指针是不是 NULL;
第三点: 指针使用后, 赋值为 NULL;
第四点: 在指针使用之前, 将其赋值绑定给一个可用地址空间;
以上四点绝对可行, 但略显麻烦, 实际中的处理方法: 在中小型程序中, 自己水平能把握的情况下, 不必完全参照这个标准; 但大型程序或者自己水平感觉不好把握时, 建议严格参照这个方法;
NULL 到底是什么:
NULL 在 C/C++ 中定义为:
在 C 语言中, int *p; 可以 p=(int *)0; 但是不可以 p=0; 因为类型不同;
NULL 的实质就是 0, 然后我们给指针赋初值为 NULL, 其实就是让指针指向 0 地址处
为什么是 0 地址处?
原因 1: 0 地址处作为一个特殊地址(我们认为指针指向这里就表示指针没有被初始化, 就表示是野指针);
原因 2: 0 地址在一般的操作系统中都是不可被访问的, 如果 C 语言程序员不按规矩 (不检查是否等于 NULL 就去解引用) 写代码, 直接去解引用就会触发段错误, 这种已经是最好的结果了;
一般在代码段指针是否是野指针时, 都写成: if(NULL !=p) 而不是写成 if(p !=NULL)原因: 如果 NULL 写在后面, 当中间是 == 号的时候, 有时候容易忘记写成了 =, 这时候其实程序已经错误, 但是编译器不会报错这个错误 (对新手) 很难检查出来; 如果习惯了把 NULL 写在前面, 当错误的把 == 写成了 = 时, 编译器会报错, 程序员会发现这个错误
const 关键字与原则
const 修饰指针的 4 种形式
const 关键字, 在 C 语言中用来修饰变量, 表示这个变量是常量
const 修饰指针有 4 种形式, 区分清楚这 4 种即可全部理解 const 和指针
- const int *p; // p 可变, p 指向的对象不可变
- int const *p; // p 可变, p 指向的对象不可变
- int *const p; // p 不可变, p 指向的对象可变
- const int *const p; // 指针 p 和 p 指向的对象都不可变
记忆方法: 先忽略类型名, 看 const 离哪个近, 离谁近, const 就修饰谁, 被修饰的符号不可更改;
关于指针变量的理解, 主要涉及到 2 个变量: 第一个是指针变量 p 本身, 第二个是 p 指向的那个变量 (*p) 一个 const 关键字只能修饰一个变量, 所以弄清楚这 4 个表达式的关键就是搞清楚 const 放在某个位置是修饰谁的
- typedef int * pint ; const pint p ;//p 不可更改, 但 p 指向的内容可更改
- #define PINT int * ; const PINT p ;//p 可更改, 但是 p 指向的内容不可更改
具体区别查看网址: https://zhidao.baidu.com/question/46929256.html,http://www.cnblogs.com/afarmer/archive/2011/05/05/2038201.html
const 修饰的变量真的不能改吗?
课堂练习说明: const 修饰的变量其实是可以改的(前提是 gcc 环境下)
在某些单片机环境下, const 修饰的变量是不可以改的 const 修饰的变量到底能不能真的被修改, 取决于具体的环境, C 语言本身并没有完全严格一致的要求
在 gcc 中, const 是通过编译器在编译的时候执行检查来确保实现的 (也就是说 const 类型的变量不能改是编译错误, 不是运行时错误) 所以我们只要想办法骗过编译器, 就可以修改 const 定义的常量, 而运行时不会报错
更深入一层的原因, 是因为 gcc 把 const 类型的常量也放在了 data 段, 其实和普通的全局变量放在 data 段是一样实现的, 只是通过编译器认定这个变量是 const 的, 运行时并没有标记 const 标志, 所以只要骗过编译器就可以修改了
const 修饰的变量可以使用 指针 方式进行修改;
const 究竟应该怎么用
const 是在编译器中实现的, 编译时检查, 并非不能骗过所以在 C 语言中使用 const, 就好象是一种道德约束而非法律约束, 所以大家使用 const 时更多是传递一种信息, 就是告诉编译器也告诉读程序的人, 这个变量是不应该也不必被修改的
深入学习数组
从内存的角度理解数组:
从内存角度讲, 数组变量就是一次分配多个变量, 而且这多个变量在内存中的存储单元是依次相连接的
我们分开定义多个变量 (譬如 int a, b, c, d;) 和一次定义一个数组(int a[4]); 这两种定义方法相同点是都定义了 4 个 int 型变量, 而且这 4 个变量都是独立的单个使用的; 不同点是单独定义时 abcd 在内存中的地址不一定相连, 但是定义成数组后, 数组中的 4 个元素地址肯定是依次相连的
数组中多个变量虽然必须单独访问, 但是因为他们的地址彼此相连, 因此很适合用指针来操作, 因此数组和指针天生就叫纠缠在一起
从编译器角度理解数组:
从编译器角度来讲, 数组变量也是变量, 和普通变量和指针变量并没有本质不同变量的本质就是一个地址, 这个地址在编译器中决定具体数值, 具体数值和变量名绑定, 变量类型决定这个地址的延续长度
搞清楚: 变量变量名变量类型这三个概念的具体含义, 很多问题都清楚了
int a; char a;
数组中几个关键符号(a a[0] &a &a[0])
这 4 个符号搞清楚了, 数组相关的很多问题都有答案了理解这些符号的时候要和左值右值结合起来, 也就是搞清楚每个符号分别做左值和右值时的不同含义
a 就是数组名 a 做左值时表示整个数组的所有空间(10×4=40 字节), 又因为 C 语言规定数组操作时要独立单个操作, 不能整体操作数组, 所以 a 不能做左值; a 做右值表示数组首元素的首地址(数组的第 0 个元素, 也就是 a[0]; 首地址就是起始地址, 就是 4 个字节中最开始第一个字节的地址)a 做右值等同于 & a[0];
a[0]表示数组的首元素, 也就是数组的第 0 个元素做左值时表示数组第 0 个元素对应的内存空间(连续 4 字节); 做右值时表示数组第 0 个元素的值(也就是数组第 0 个元素对应的内存空间中存储的那个数)
&a 就是数组名 a 取地址, 字面意思来看就应该是数组的地址 & a 不能做左值(&a 实质是一个常量, 不是变量因此不能赋值, 所以自然不能做左值);&a 做右值时表示整个数组的首地址
&a[0]字面意思就是数组第 0 个元素的首地址 (搞清楚[] 和 & 的优先级,[]的优先级要高于 &, 所以 a 先和 [] 结合再取地址)做左值时表示数组首元素对应的内存空间, 做右值时表示数组首元素的值 (也就是数组首元素对应的内存空间中存储的那个数值) 做右值时 & a[0]等同于 a
解释: 为什么数组的地址是常量? 因为数组是编译器在内存中自动分配的当我们每次执行程序时, 运行时都会帮我们分配一块内存给这个数组, 只要完成了分配, 这个数组的地址就定好了, 本次程序运行直到终止都无法再改了那么我们在程序中只能通过 & a 来获取这个分配的地址, 却不能去 x 用赋值运算符修改它
总结:
&a 和 a 做右值时的区别:&a 是整个数组的首地址, 而 a 是数组首元素的首地址这两个在数字上是相等的, 但是意义不相同意义不相同会导致他们在参与运算的时候有不同的表现
a 和 & a[0]做右值时意义和数值完全相同, 完全可以互相替代
&a 是常量, 不能做左值(a 做左值代表整个数组所有空间, 所以 a 不能做左值)
指针与数组的天生姻缘
以指针方式访问数组元素:
数组元素使用时不能整体访问, 只能单个访问访问方式有 2 种: 数组形式和指针形式
数组格式访问数组元素是: 数组名[下标]; (注意下标从 0 开始)
指针格式访问数组元素是:*(指针 + 偏移量); 如果指针是数组首元素地址(a 或者 & a[0]), 那么偏移量就是下标; 指针也可以不是首元素地址而是其他哪个元素的地址, 这时候偏移量就要考虑叠加了
数组下标方式和指针方式均可以访问数组元素, 两者的实质其实是一样的在编译器内部都是用指针方式来访问数组元素的, 数组下标方式只是编译器提供给编程者一种壳 (语法糖) 而已所以用指针方式来访问数组才是本质的做法
从内存角度理解指针访问内存的实质:
数组的特点就是: 数组中各个元素的地址是依次相连的, 而且数组还有一个很大的特点 (其实也是数组的一个限制) 就是数组中各个元素的类型比较相同类型相同就决定了每个数组元素占几个字节是相同的(譬如 int 数组每个元素都占 4 字节, 没有例外)
数组中的元素其实就是地址相连接占地大小相同的一串内存空间这两个特点就决定了只要知道数组中一个元素的地址, 就可以很容易推算出其他元素的地址
指针和数组类型的匹配问题:
- int *p; int a[5]; p=a; // 类型匹配, a 表示数组首元素的首地址, 为 int * 类型
- int *p; int a[5]; p=&a; // 类型不匹配,&a 表示数组首地址, 不是 int 类型, 是 int(*)[5]类型
&aa&a[0]从数值上来看是完全相等的, 但是从意义上看就不同了: 从意义上看, a 和 & a[0]是元素的指针, 也就是 int * 类型; 而 & a 是数组指针, 是 int (*)[5] 类型;
总结: 指针类型决定了指针如何参与运算:
指针参与运算时, 因为指针变量本身存储的数值是表示地址的, 所以运算也是地址的运算;
指针参与运算的特点: 指针变量 + 1, 并不是真的 + 1, 而是加 1*sizeof(指针类型); 如果是 int * 指针, 则 + 1 实际表示的是地址 + 4, 如果是 char * 指针, 则 + 1 就表示地址 + 1; 如果是 double * 指针, 则 + 1 就表示地址 + 8;
指针变量 + 1 实际不是 + 1, 而是加 1×sizeof(指针类型), 主要原因是希望指针 + 1 后刚好指向下一个元素(而不是希望错位);
指针与强制类型转换
变量的数据类型的含义:
所有的类型的数据存储在内存中, 都是按照二进制格式存储的所以内存中只知道有 0 和 1, 不知道是 int 的还是 float 的还是其他类型
intcharshort 等属于整形, 他们的存储方式 (数转换成二进制往内存中放的方式) 是相同的, 只是内存格子大小不同(所以这几种整形就彼此叫二进制兼容格式); 而 float 和 double 的存储方式彼此不同, 和整形更不同
int a = 5; 时, 编译器给 a 分配 4 字节空间, 并且将 5 按照 int 类型的存储方式转成二进制存到 a 所对应的内存空间中去 (a 做左值的); 我们 printf 去打印 a 的时候(a 此时做右值),printf 内部的 vsprintf 函数会按照格式化字符串(就是 printf 传参的第一个字符串参数中的 %d 之类的东西) 所代表的类型去解析 a 所对应的内存空间, 解析出的值用来输出也就是说, 存进去时是按照这个变量本身的数据类型来存储的 (譬如本例中 a 为 int 所以按照 int 格式来存储); 但是取出来时是按照 printf 中 %d 之类的格式化字符串的格式来提取的此时虽然 a 所代表的内存空间中的 10101 序列并没有变(内存是没被修改的) 但是怎么理解 (怎么把这些 1010 转成数字) 就不一定了譬如我们用 %d 来解析, 那么还是按照 int 格式解析则值自然还是 5; 但是如果用 %f 来解析, 则 printf 就以为 a 对应的内存空间中存储的是一个 float 类型的数, 会按照 float 类型来解析, 值自然是很奇怪的一个数字了
总结: C 语言中的数据类型的本质, 就是决定了这个数在内存中怎么存储的问题, 也就是决定了这个数如何转成二进制的问题一定要记住的一点是内存只是存储 1010 的序列, 而不管这些 1010 怎么解析所以要求我们平时数据类型不能瞎胡乱搞
分析几个题目:
按照 int 类型存却按照 float 类型取 一定会出错
按照 int 类型存却按照 char 类型取 有可能出错也有可能不出错
按照 short 类型存却按照 int 类型取 有可能出错也有可能不出错
按照 float 类型存却按照 double 取 一定会出错
指针的数据类型的含义:
指针的本质是: 变量, 指针就是指针变量
一个指针涉及 2 个变量: 一个是指针变量自己本身, 一个是指针变量指向的那个变量
int *p; 定义指针变量时, p(指针变量本身)是 int * 类型,*p(指针指向的那个变量)是 int 类型的
int * 类型说白了就是指针类型, 只要是指针类型就都是占 4 字节, 解析方式都是按照地址的方式来解析 (意思是里面存的 32 个二进制加起来表示一个内存地址) 的结论就是: 所有的指针类型 (不管是 int * 还是 char * 还是 double *) 的解析方式是相同的, 都是地址
对于指针所指向的那个变量来说, 指针的类型就很重要了指针所指向的那个变量的类型 (它所对应的内存空间的解析方法) 要取决于指针类型譬如指针是 int * 的, 那么指针所指向的变量就是 int 类型的
指针数据类型转换示例分析 1:(int * -> char *)
int 和 char 类型都是整形, 类型兼容的所以互转的时候有时候错有时候对
int 和 char 的不同在于 char 只有 1 个字节而 int 有 4 个字节, 所以 int 的范围比 char 大在 char 所表示的范围之内 int 和 char 是可以互转的不会出错; 但是超过了 char 的范围后 char 转成 int 不会错(向大方向转就不会错, 就好比拿小瓶子的水往大瓶子倒不会漏掉不会丢掉), 而从 int 到 char 转就会出错(就好象拿大瓶子水往小瓶子倒一样)
指针数据类型转换示例分析 2:(int * -> float *)
之前分析过: int 和 float 的解析方式是不兼容的, 所以 int * 转成 float * 再去访问绝对会出错
指针数组与 sizeof 运算符
sizeof 是 C 语言的一个运算符 (主要 sizeof 不是函数, 虽然用法很像函数),sizeof 的作用是用来返回() 里面的变量或者数据类型占用的内存字节数
sizeof 存在的价值? 主要是因为在不同平台下各种数据类型所占的内存字节数不尽相同 (譬如 int 在 32 位系统中为 4 字节, 在 16 位系统中为 2 字节???) 所以程序中需要使用 sizeof 来判断当前变量 / 数据类型在当前环境下占几个字节
比较下面语句的结果:
- char str[] = hello; sizeof(str); sizeof(str[0]); strlen(str);
- char *p=str; sizeof(p); sizeof(*p); strlen(p);
32 位系统中所有指针的长度都是 4, 不管是什么类型的指针
strlen 是一个 C 库函数, 用来返回一个字符串的长度 (注意, 字符串的长度是不计算字符串末尾的 \ 0 的) 一定要注意 strlen 接收的参数必须是一个字符串(字符串的特征是以 \ 0 结尾)
sizeof 用来测试字符数组的大小, 计算字符个数要加上末尾的 \0
- int n=10; // 相当于 sizeof(n) ,sizeof 测试一个变量本身与测试这个变量的类型, 其结果是一样的;
- int b[100]; // 相当于 sizeof(b) ,sizeof(数组名)的时候, 变量名不做左值, 也不做右值, 纯粹是数组名的含义那么 sizeof(数组名)实际返回的是整个数组所占的内存空间(以字节为单位);
数组作函数形参
函数传参, 形参是可以用数组的
函数形参是数组时, 实际传递是不是整个数组, 而是数组的首元素首地址也就是说函数传参用数组来传, 实际相当于传递的是指针(指针指向数组的首元素首地址)
函数在传递数组时, 需要传递大小, 因为数组在传递的时候不能传递大小, 数组在经过函数调用后, 数组的大小就丢了, 只剩下首地址;
define 和 typedef
#define 是宏命令, 在编译前, 由预处理器做替代, 如同文本编辑的替代命令, 把程序中的所有遇到的词, 全部替代;
typedef int* pint; 是语句, 由编译器在编译过程中编译处理
- typedef int * pint ; const pint p ;//p 不可更改, 但 p 指向的内容可更改
- #define PINT int * ; const PINT p ;//p 可更改, 但是 p 指向的内容不可更改
具体区别查看网址: https://zhidao.baidu.com/question/46929256.html,http://www.cnblogs.com/afarmer/archive/2011/05/05/2038201.html
输入型参数与函数型参数
普通变量作为函数形参:
在子函数内部, 形参的值等于实参原因是函数调用时把实参的值赋值给了形参
这就是很多书上写的传值调用(相当于实参做右值, 形参做左值)
数组作为函数形参:
数组名作为形参传参时, 实际传递是不是整个数组, 而是数组的首元素的首地址 (也就是整个数组的首地址因为传参时是传值, 所以这两个没区别) 所以在子函数内部, 传进来的数组名就等于是一个指向数组首元素首地址的指针所以 sizeof 得到的是 4.
在子函数内传参得到的数组首元素首地址, 和外面得到的数组首元素首地址的值是相同的很多人把这种特性叫做传址调用(所谓的传址调用就是调用子函数时传了地址(也就是指针), 此时可以通过传进去的地址来访问实参)
数组作为函数形参时,[]里的数字是可有可无的为什么? 因为数组名做形参传递的实际只是个指针, 根本没有数组长度这个信息
指针作为函数形参:
和数组作为函数形参的用法一模一样, 只需要把 (int a[]) 改为 (int *p) 即可这种方式就好像指针方式访问数组和数组方式访问数组元素的结果一样, 是一样的
结构体作为函数形参:
结构体变量作为函数形参的时候, 实际上和普通变量 (类似于 int 之类的) 传参时表现是一模一样的, 所以说结构体变量其实也是普通变量而已;
- struct dd
- {
- int a;
- int b;
- float c;
- double d;
- };
- void func1(struct dd *p)
- {
- printf("sizeof(p) = %d\n",sizeof(p)); //4
- printf("sizeof(*p) = %d\n",sizeof(*p)); //20
- printf("&p = %p\n",&p); //0xbffa0f70
- printf("p->c = %f\n",p->c); //6.600000
- }
- int main(void)
- {
- struct dd a =
- {
- .a = 4,
- .b = 5,
- .c = 6.6,
- .d = 7.7,
- };
- printf("sizeof(a) = %d\n",sizeof(a)); //20
- printf("&a = %p\n",&a); //0xbfb6e3c8
- printf("a.b = %d\n",a.b); //5
- func1(&a);
- return0;
- }
结构体因为自身太大, 所以传参应该用指针来传(但是程序员可以自己决定, 你非要传结构体变量过去 C 语言也是允许的, 只是效率低了); 回想一下数组, 为什么 C 语言设计的时候数组传参默认是传的数组首元素首地址而不是整个数组?(因为传数组效率低, C 语言帮我们做了决定, 而结构体需要我们自己做决定)
传值调用与传址调用:
传值调用描述的是这样一种现象: x 和 y 作为实参, 自己并没有真身进入 swap1 函数内部, 而只是拷贝了一份自己的副本 (副本具有和自己一样的值, 但是是不同的变量) 进入子函数 swap1, 然后我们在子函数 swap1 中交换的实际是副本而不是 xy 真身所以在 swap1 内部确实是交换了, 但是到外部的 x 和 y 根本没有受影响
- void swap1(int a, int b)
- {
- int tmp;
- tmp = a;
- a = b;
- b = tmp;
- printf("in swap1, a = %d, b = %d.\n", a, b);
- }
- intmain(void)
- {
- int x = 3, y =5;
- swap2(x, y);
- printf("x = %d, y = %d.\n", x, y);
- }
在 swap2 中 x 和 y 真的被改变了 (但是 x 和 y 真身还是没有进入 swap2 函数内, 而是 swap2 函数内部跑出来把外面的 x 和 y 真身改了) 实际上实参 x 和 y 永远无法真身进入子函数内部(进去的只能是一份拷贝), 但是在 swap2 我们把 x 和 y 的地址传进去给子函数了, 于是乎在子函数内可以通过指针解引用方式从函数内部访问到外部的 x 和 y 真身, 从而改变 x 和 y
- void swap2(int *a, int *b)
- {
- int tmp;
- tmp = *a;
- *a = *b;
- *b = tmp;
- printf("in swap1, *a = %d, *b = %d.\n", *a, *b);
- }
- intmain(void)
- {
- int x = 3, y =5;
- swap2(&x, &y);
- printf("x = %d, y = %d.\n", x, y); // 交换成功
- }
结论: 这个世界上根本没有传值和传址这两种方式, C 语言本身函数调用时一直是传值的, 只不过传的值可以是变量名, 也可以是变量的指针
输入型参数与输出型参数
函数为什么需要形参与返回值:
函数名是一个符号, 表示整个函数代码段的首地址, 实质是一个指针常量, 所以在程序中使用到函数名时都是当地址用的, 用来调用这个函数的
函数体是函数的关键, 由一对 {} 括起来, 包含很多句代码, 函数体就是函数实际做的工作
形参列表和返回值形参是函数的输入部分, 返回值是函数的输出部分对函数最好的理解就是把函数看成是一个加工机器(程序其实就是数据加工器), 形参列表就是这个机器的原材料输入端; 而返回值就是机器的成品输出端
其实如果没有形参列表和返回值, 函数也能对数据进行加工, 用全局变量即可用全局变量来传参和用函数参数列表返回值来传参各有特点, 在实践中都有使用总的来说, 函数参数传参用的比较多, 因为这样可以实现模块化编程, 而 C 语言中也是尽量减少使用全局变量
全局变量传参最大的好处就是省略了函数传参的开销, 所以效率要高一些; 但是实战中用的最多的还是函数传参, 如果参数很多传参开销非常大, 通常的做法是把很多参数打包成一个结构体, 然后传结构体变量指针进去
函数传参中使用 const 指针:
const 一般在函数参数列表中, 用法是 const int *p;(意义是指针变量本身是可变的, 指针指向的变量类型是不可变的);
const 用来修饰指针做函数传参, 作用在于申明函数内部不会改变这个指针所指向的内容, 所以给函数传一个不可改变的指针 (char *p = "linux"; 这种) 不会触发错误; 而一个为申明位 const 的指针的函数, 给他传一个不可更改的指针的时候就要小心了;
const 的用途:
定义常量: 被 const 修饰过的变量不能被修改, 故此具有常量之称如果类的成员变量是常量, 那么在初始化的时候必须初始化
修饰函数: const 可以修饰函数的返回值, 参数及, 函数的定义体, 被 const 修饰会受到强制的保护, 能防止意外的修改, 从而提高函数的健壮性
1. 修饰参数: 不能在定义体中修改形参的值
2. 修饰返回值: 被修饰的返回值不能作为左值, 只有作为右值使用
3. 修饰函数定义体: 被 const 修饰的函数定义体的函数能被 const 或者非 const 对象调用, 但是 const 对象只能调用被 const 修饰过定义体的函数
函数需要向外部返回多个值怎么办:
一般来说, 函数的输入部分就是函数的参数, 输出部分就是函数的返回值可问题是: 函数的参数可以有多个, 但函数的返回值只有 1 个, 这就使得我们无法让一个函数返回多个值;
在编程中, 使用一个函数返回多个返回值的用法是非常普遍的, 因为安全依赖于返回值是不靠谱的, 通常的做法是用参数来返回(在经典的 linux 风格中, 返回值是不用来返回结果的, 而是返回 0 或者负数来表示程序执行结果是对还是错, 是成功还是失败);
普遍做法: 编程中函数的输入和输出都是靠函数参数的, 返回值只是用来表示函数执行的结果是对 (成功) 还是错 (失败) 如果参数是用来输入的, 就叫输入型参数; 如果这个参数的目的是用来做输出的, 就叫输出型参数;
输出型参数就是用来让函数内部把数据输出到函数外部的
总结:
看到一个函数的原型后, 怎么样一眼就看出来哪个参数做输入, 哪个参数做输出? 函数传参如果传的是普通变量(不是指针), 那肯定是输入型参数;
如果传指针就有 2 种可能性, 为了区别, 经常做法是: 如果这个参数是做输入就在指针前面加 const 来修饰;(通常做输入的在函数内部只需要读取这个参数而不会需要更改他), 如果函数形参是指针变量并且还没加 const, 那就表示这个参数是用来做输出型参数的
譬如 C 库函数中 strcpy 函数: 网址 http://blog.csdn.net/wconvey/article/details/21150103
来源: http://www.bubuko.com/infodetail-2521869.html