ARM—C语言学习笔记(零)指针与数组

(一)wild_pointer

(1)野指针

​ 指针指向的位置是不可知的(随机的、不明确的、没有明确限制)

(2)野指针指向未知地址有三种情况

​ (1)指向不可访问(操作系统不允许访问的敏感地址,比如内核空间)的地址,结果是触发段错误。(这算比较好的情况)

​ (2)指向一个可用但没有特别意义的空间(比如我们曾经使用过的但是已经不用的栈空间或者堆空间),这时候程序不会报错,也不会对当前程序造成损害,这种情况会掩盖你的程序的错误,让你以为程序没问题,其实是有问题的。

​ (3)指向一个可用的空间,而且这个空间其实在程序中正在被使用(比如一个程序的变量x),那么野指针的解引用就会刚好修改这个变量x的值导致这个变量莫名奇妙被改变,程序出现离奇错误。一般都会导致程序崩溃,或者数据被损害,这种危害是最大的。

(3)怎样避免野指针?

​ (1)野指针的错误就是指针定义后没有初始化,也就是没有赋值(总之就是指针没有明确的指向一个可用的内存空间),然后去解引用。

​ (2)理解了原因,避免的方法就来了:在指针解引用之前 ,一定确保指针指向一个绝对可用的空间。

​ (3)常规做法:

​ 1.定义指针时,同时初始化为NULL

​ 2.在指针解引用之前,先去判断这个指针是不是NULL

​ 3.指针使用完之后,将其 赋值为NULL

​ 4.指针使用之前,将其赋值给一个可用的地址空间

#include <stdio.h>
int main (void)
{
    int a;
    int *p = NULL;        
    //中间省略400行代码......
    //p = (int *)4     //4地址不是你确定可以访问的,就不要用指针去解引用
    p = &a;            //正确使用的指针方式,是解引用指针前跟一个绝对可用的地址绑定 
    if(NULL != p)        
    {
        *p = 4;
    }
    p = NULL;           
    
    return 0;
}

NULL是什么?

//NULL在C/C++中的定义
#ifndef _cplusplus        //定义这个符号代表C++环境
#define NULL 0            //在C++中NULL就是0
#else
#define NULL (void *)0    //在C中NULL是强制类型转化为void *的0
#endif

NULL 的 实 质 其 实 就 是 0 , 然 后 我 们 给 指 针 赋 初 值 为 NULL , 其 实 就 是 让 指 针 指 向 0地址处,为 什 么 指 向 0 地 址 处 ? 2 个 原 因 。

(1)第 一 层 原 因 是 0 地 址 处 作 为 一 个 特 殊 地 址 ( 我 们 认 为 指 针 指 向 这 里 就 表 示 指 针 没 有 被 初 始 化 ,就 表 示 是 野 指 针 );

(2)第 二 层 原 因 是 这 个 地 址 0 地 址 在 一 般 的 操 作 系 统 中 都 是 不 可 被 访 问 的 ,如 果 c 语 言 程 序 员 不 按 规 矩 ( 不 检 查 是 否 等 于NULL 就 去 解 引 用 )写 代 码 直 接 去 解 引 用 就 会 触 发 段 错 误 ,这 种 己 经 是 最 好 的 结 果 了 。

2019.11.19

(二)const关键字与指针

(1)const修饰指针的4种形式

(1)const关键字,在c语言中用来修饰变量,表示这个变量是常量。

(2)const修饰指针有4种形式,区分清楚这4种即可全部理解const和指针。

第一种 const int *p
第二种 int const *p
第三种 int * const p
第四种 const int const pi

(3)关于指针变量的理解,主要涉及到2个变量:第一个是指针变量p本身,第二个是p指向的那个变量(*p)。一个const关键字只能修饰一个变量,所以弄清楚这4个表达式的关键就是搞清楚const放在某个位置是修饰谁的。

# include <stdio.h>
int main (void)
{
	int const a = 5;
	int  *p;
	//a = 6;               //error: assignment of read-only variable ‘a’
	//p = &a;              //warning: assignment discards ‘const’ qualifier from pointer target type                              [-Wdiscarded-qualifiers]
	p = (int *)&a;         //骗过编译器不报错
	*p =  6;
	printf("p = %d.\n", a);

	/*
	int a = 5;
	//第一种
	const int *p1;         //p本身不是const的,而p指向的变量是const的
	//第二种
	int const *p2;         //p本身不是const的,而p指向的变量是const的
	//第三种
	int * const p3;        //p本身是const的,而p指向的变量不是const的
	//第四种
	const int * const p4;  //p本身是const的,而p指向的变量不是const的

	*p1 =  3;              //error: assignment of read-only location ‘*p1’
	p1 = &a;

	*p2 =  3;              //error: assignment of read-only location ‘*p2’
	p2 = &a;

	*p3 =  3;            
	p3 = &a;               //error: assignment of read-only variable ‘p3’

	*p4 =  3;              //error: assignment of read-only location ‘*p4’
	p4 = &a;               //error: assignment of read-only variable ‘p4’
	*/
    
	return 0;
} 

(2)const修饰的变量真的不能改吗?

(1)上面代码说明:const修饰的变量其实是可以改的(前提是gcc环境下)

(2)在某些单片机环境下, const修饰的变量是不可以改的。const修饰的变量到底能不能真的被修改,取决于具体的环境, c语 言本身并没有完全严格一致的要求。

(3)在gcc中,const是通过编译器在编译的时候执行检查来确保实现的(也就是说const类型的变量不能改是编译错误,不是运行时错误。)所以我们只要想办法骗过编器,就可以修改const定义的量,而运行时不会报错。

(4)更深入一层的原因,是因为gcc把const类型的常量也放在了data段,其实和普通的全局变量放在data段是一样实现的,只是通过编译认定这个变量是const的,运行时并没有标记const标志,所以只要骗过编译器就可以修改了。

(3)const究竟应该怎么用?

(1) const是在编译器中实现的,编译时检查,并非不能骗过。所以在c语言中使用const,就好像是一种道德约束而非法律约束,所以大家使用const时更多是传递一种信息,就是告诉编译器,也告诉读程序的人,这个变量是不应该也不必被修改。

2019.11.21

(三)深入学习数组

(1)从内存角度来理解数组

(1)从内存角度讲,数组变量就是一次分配多个变量,而且这多个变量在内在中的存储单元是依次相连接的。

(2)我们分开定义多个变量(譬如int a, b, c, d;)和一次定义一个数组(int a[4]) ;这两种定义方法相同点是都定义了4个int型变量,而且这4个变量都是独立的单个使用的;不同点是单独定义时a、b、c、d在内存中的地址不一定相连,但是定义成数组后,数组中的4个元素地址肯定是依次相连的。

(3)数组中多个变量虽然必须单独访问,但是因为他们的地址彼此相连,因此很适合用指钍来操作,因此数组和指针天生就叫纠结在一起。

(2)从编译器角度来理解数组

(1)从编译器角度来讲,数组变量也是变量,和普通变量和指针变量并没有本质不同。变量的本质就是一个地址,这个地址在编译器中决定具体数值,具体数值和变量名绑定,变量类型决定这个地址的延续长度。

(2)搞清楚:变量,变量名、变量类型这三个概念的具体含义,很多问题都清楚了。

(3)数组中几个关键符号(a、 a[0]、 &a 、&a[0])的理解

(1)这4个符号搞清楚了,数组相关的很多问题都有答案了。理解这些符号的时候要和左值右值结合起来,也就是搞清楚每个符号分别做左值和右值时的不同含义。

(2)a就是数组名。a做左值时表示整个数组的所有空间(10x4=40字节) ,又因为C语言规定数组操作时要独立单个操作,不能整体操作数组,所以a不能做左值; a做右值表示数组首元素(数组的第0个元素,也就是a[0])的首地址(首地址就是起始地址,就是4个字节中最开始第一个字节的地址) 。a做右值等同于&a[0];

2.a[0]表示数组的首元素,也就是数组的第0个元素。做左值时表示数组第0个元素对应的内存空间(连续4字节) ;做右值时表示数组第0个元素的值(也就是数组第0个元素对应的内存空间中存储的那个数)。

(3)&a就是数组名a取地址,字面意思来看就应该是数组的地址.a不能做左值(&a实质是一个常量,不是变量因此不能赋值,所以自然不能做左值。)

解释:为什么数组的地址是常量?因为数组是编译器在内在中自动分配的。当我们每次执行程序时,运行时都会,我们分配一块内存给这个数组,只要完成了分配,这个数组的地址就定好了,本次程序运行直到终止都无法再改了。那么我们在程序中只能通过a来获取这个分配的地址,却不能去用赋值运算修改它。

(4)总结

(1)&a和a做右值时的区别:&a是整个数组的首地址,而a是数组首元素的首地址。这两个在数字上是相等的,但是意义不相。意义不相同会导致他们在参与运算的时候有不同的表现。

(2)a和&a[0]做右值时意义和数值完全相同,完全可以相互替代。

(3)&a是常量,不能做左值。

(4)a做左值代表整个数组所有空间,所以a不能做左值。

2019.11.26

(四)指针与数组的天生姻缘

(1)以指针方式来访问数组元素

(1)数组元素使用时不能整体访问,只能单个访问。访问方式有2种:数组形式和指针形式。

(2)数组格式访问数组元素是:组名[下标]; (注意下标从0开始)

(3)指针格式访问数组元素是* (指针+偏移量);

如果指针是数组首元素地址(a或煮&a [0]),那么偏移量就是下标:指针也可以不是首元素地址而是其他哪个元素的地址,这时候偏移量就要考虑叠加工。

(4)数组下标方式和指针方式均可以访问数组元素,两者的实质其实是一样的。在编译器内部都是以指针方式来访问数组元素的,数组下标方式只是编译器提供给编程者一种壳(语法糖)而已

(2)指针和数组类型的匹配问题

(1)int *p; int a[5]; p=a; //类型匹配

int *p; int a[5]; p=&a; //类型不匹配是。p是int

*, &a是整个数组的指针,也就是一个数组指针类型,不是int指针类型,所以不匹配

(2)&a 、a 、&a[0]从数值上来看是完全相等的,但是意义来看就不相同了,从意义上来看,a和&a[0]是数组首元素首地址,而&a是整个数组的首地址:从类型来看,a和&a[0]是元素的指针,也就是int 类型,而&a是数组指针,是int () [5] ;类型

(3)总结:指针类型决定了指针如何参与运算

(1)指针参与运算时,因为指针变量本身在储的数值是表示地址的,所以运算也是地址的运算

(2)指针参与运算的特点是,指针变量+1,并不是真的加1,而是加1sizeof (指针类型);如果是int指t,则+1就实际表示地址+4,如果是char 指针,则+1就表示地址+1;如果是doublef针,则+1就表示地址+8.

  • code

    #include <stdio.h>
    int main(void)
    {
    	int a[5] = {1, 2, 3, 4, 5};
    	int *p;
    	p = a;
    	printf("1 *(p + 1) = %d.\n", *(p + 1));
    	printf("2 *(p + 1) = %d.\n", *((char *)p + 1));
    	printf("3 *(p + 1) = %d.\n", *((int *)((unsigned int)p + 1)));
    
    	char *p2;
    	p2 = (char *)p;
    	printf("4 *(p + 1) = %d.\n", *(p + 1));
    
    /*
    	int a[5] = {1, 2, 3, 4, 5};
    	int *p;
    	p = a;
    	printf("a = %x.\n", a);
    	printf("&a = %x.\n", &a);
    	printf("&a[0] = %x.\n", &a[0]);
    	printf("a[0] = %x.\n", a[0]);
    */
    
    
    
    /*
    	int a[5] = {1, 2, 3, 4, 5};
    	printf("a[4] = %d.\n", a[4]);
    	printf("*(a+4) = %d.\n", *(a+4));
    
    	int *p;
    	p = a; //a做右值表示数组元素首地址
    	printf("*(p + 4) = %d.\n", *(p + 4));
    
    
    	
    	p = &a[2];
    	printf("*(p + 2) = %d.\n", *(p + 2));
    	printf("*(p + 4) = %d.\n", *(p + 4)); //已经越界
    
    */
    	return 0;
    }
    
    

2020.2.11

(五)指针与强制类型转换

(1)变量的数据类型的含义

(1)所有的类型的数据存储在内存中,都是按照二进制格式存储的。所以内存中只知道有0和1,不知道是int的、还是float的还是其他类型。

(2)int 、char 、short等属于整形,他们的在储方式(数转换成二进制往内存中放的方式)是相同的,只是内存格子大小不同(所以这几种整形就彼此叫二进制兼容格式);而float和double的储存方式彼此不同,和整形更不同。

(3)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类型来解析,值自然是很奇怪的一个数字了.

(4)总结: C语言中的数据类型的本质,就是决定了这个数在内存中怎么存储的问题,也就是决定了这个数如何转成二进制的问题。**一定要记住的一点是内存只是存储1010的序列,而不管这些1010怎么解析。**所以要求我们平时数据类型不能瞎胡乱搞。

  • 分析几个题目

  • 按照int类型存却按照float类型取 一定会出错

  • 按照int类型存却按照char类型取 有可能出错也有可能不出错

  • 按照short类型存却按照int类型取 直可能出错也有可能不出错

  • 按照float类型存却按照double取 一定会出错

(2)指针的数据类型的含义

(1)指针的本质是 变量,指针就是指针变量

(2)一个指针涉及2个变量:一个是指针变量自己本身,一个是指针变量指向的那个变量

(3)int *p;定义指针变量时,p(指针变量本身)是int类型,*(指针指向的那个变量)是int类型的

(4)int*类型说白了就是指针类型,*只要是指针类型就都是占4字节,解析方式都是按照地址的方式来解析(意思是里面存的32个二进制加起来表示一个内存地址)的。结论就是:所有的指针类型(不管是int * 还是char * 还是double *)的解析方式是相同的,都是地址

(5)对于指针所指向的那个变量来说,指针的类型就很重要了,指针所指向的那个变量的类型(它所对应的内存空间的解析方法)要取决于指针类型譬如指针是int*的,那么指针所指向的变量就是int类型的

指针数据类型转换实例分析1 (int * -> char *)

(1)int和char类型都是整形,类型兼容的。所以互转的时候有时候错有时候对.

(2)int和char的不同在于char只有1个字节而int有4个字节,所以int的范围比char大。在char所表示的范围之内int和char是可以互转的不会出错;但是超过了char的范围后char转成int不会错(向大方向转就不会错,就好比拿小瓶子的水往大瓶子倒不会漏掉不会丢掉) ,而从int到charl转就会出错(就好象拿大瓶子水往小瓶子倒一样)

(3)指针数据类型转换实例分析2 (int * -> float *)

(1)之前分析过int和float的解析方式是不兼容的,所以int * 转成float *再去访问绝对会出错

  • code
#include <stdio.h>
int main (void)
{
	int a[2] = {0x11223344, 0x55667788};
	int *p1 = a;
	printf("*p1 = 0x%x.\n", *p1);
	
	char *p2 = (char *)a;
	printf("*p2 = 0x%x.\n", *p2);
	printf("*p2 = 0x%x.\n", *(p2 + 1));
	printf("*p2 = 0x%x.\n", *(p2 + 2));
	printf("*p2 = 0x%x.\n", *(p2 + 3));
	printf("*p2 = 0x%x.\n", *(p2 + 4));
	printf("*p2 = 0x%x.\n", *(p2 + 5));
	
	
/*	
	//将int* 类型按char * / short *类型解析---有可能正确也有可能错误
	int a = 666;
	char *p1 = &a;
	printf("*p1 = %d.\n", *p1);
	
	short *p2 = &a;
	printf("*p2 = %d.\n", *p2);
*/
	
	
/*
	//将int* 类型按float *类型解析---乱码
	int a = 5;
	int *p1 = &a;
	float *p;
	p = (float *)p1;
	printf("*p1 = %d.\n", *p1);
	printf("*p = %d.\n", *p);   //乱码
*/

/*
	//将int类型按float类型解析---乱码
	int a = 5;
	
	printf("a = %d.\n", a);
	printf("a = %f.\n", a);   //乱码
*/
	return 0;
}

2020.2.14

(六)指针、数组与sizeof运算符

(1)char str[] ="hello"; sizeof (str) sizeof (str[0]) strlen (str)

(2) char *p=str; sizeof (p) sizeof (* p) strlen (p)

1.32位系统中所有指针的长度都是4,不管是什么类型的指针。

2.strlen是一个C库函数,用来返回一个字符串的长度(注意,字符串的长度是不计算字符串末尾的“/0".的) 。一定要注意strlen接收的参数必须是一个字符串(字符串的特征是以"/0"结尾)

(3)int n=10; sizeof (n)

1.sizeof测试一个变量本身和sizeof测试这个变量的类型,结果是一样的

(4)int b[100]; sizeof(b)

1.sizeof(数组名)的时候,数组名不做左值也不做右值,纯粹就是数组名的含义那么sizeof(数组名)实际返回的是整个数组所占内存空间(以字节为单位的)

(5)

void fun(int b[100])
{
	sizeof (b);
}

1.函数传参,形参是可以用数组的

2.函数形参是数组时,实际传递是不是整个数组,而是数组的首元素首地址。也就是说函数传参用数组来传,实际相当于传递的是指针(指针指向数组的首元素首地址)。

(6)

#define dpChar char *
typedef char * tpChar;

​ define 只是单纯的替换而 typedef 相当于定义了新的数据类型

  • code

    #include <stdio.h>
    #include <string.h>
    
    #define dpChar char * 
    typedef char * tpChar;
    
    //func1 完全等同于 func
    void func(int b[])
    {
    	printf("sizeof(b) = %d.\n", sizeof(b));
    }
    void func1(int *p)
    {
    	printf("sizeof(p) = %d.\n", sizeof(p));
    }
    
    void func2(int *p , int num)
    {
    	//数组在函数传递的时候只能传递首地址 不能传递大小 
    	//要传递数组就要另外传数组的大小
    	
    	//在子函数内 p是数组的首地址
    	//在子函数内 num是数组的大小
    }
    
    int main (void)
    {
        //用这种方式来使用数组的大小 而不是 直接 b = 56;
        int a[56];
        int b = sizeof(a) / sizeof(a[0]); //整个数组字节数/数组中一个元素的字节数
        printf("b = %d.\n", b);           //结果是数组的元素个数
    /*
    	dpChar p1, p2; //展开 char *p1; char p2;
    	tpChar p3, p4; //展开 char *p3; char *p4
    	printf("sizeof(p1) = %d.\n", sizeof(p1)); //8
    	printf("sizeof(p2) = %d.\n", sizeof(p2)); //1
    	printf("sizeof(p3) = %d.\n", sizeof(p3)); //8
    	printf("sizeof(p4) = %d.\n", sizeof(p4)); //8
    */
    
    /*
    	int a[20];
    	func(a);      //8 因为a在函数func内部就是指针 而不是数组
    	func1(a);     //8
    	func2(a, sizeof(a[20])); //传递数组应该这样传
    */
    
    /*
    	int b1[100];
    	printf("sizeof(b1) = %d.\n", sizeof(b1)); //400 100*sizeof(int) 
    	short b2[100];
    	printf("sizeof(b2) = %d.\n", sizeof(b2)); //200 100*sizeof(short) 
    	char b3[100];
    	printf("sizeof(b3) = %d.\n", sizeof(b3)); //100 100*sizeof(char) 
    */
    
    /*
    	int n = 100;
    	printf("sizeof(n) = %d.\n", sizeof(n));
    	printf("sizeof(int) = %d.\n", sizeof(int));
    */
    	
    	
    	
    /*
    	char str[] = "hello";
    	printf("sizeof(str) = %d.\n", sizeof(str));       // 6  6*sizeof(char)
    	printf("sizeof(*str) = %d.\n", sizeof(str[0]));   // 1  
    	printf("strlen(str) = %d.\n", strlen(str));       // 5  
    */
    	
    	
    /*
    	char str[] = "hello";
    	char *p = str;
    	printf("sizeof(p) = %d.\n", sizeof(p));   //8  指针变量的大小是cpu的最大寻址内存空间 
    	                                          //   64位是8字节 32位是4字节 16位是2字节
    	printf("sizeof(*p) = %d.\n", sizeof(*p)); //1  *p是char类型占1字节
    	printf("strlen(p) = %d.\n", strlen(p));   //5  strlen()是一个C语言库函数
    */
    	
    	return 0;
    }
    

2020.2.16

(七)指针与函数传参

(1)普通变量作为函数形参

(1)函数传参时,普通变量作为参数时,形参和实参名字可以相同也可以不同,实际上都是用实参来替代相对应的形参的.

(2)在子函数内部,形参的值等于实参原因是函数调用时把实参的值赋值给了形参。

(3)这就是很多书上写的“传值调用"(相当于实参做右值,形参做左值)

(2)数组作为函数形参

(1)函数名作为形参传参时,实际传递的不是整个数组,而是数组首元素的首地址(也就是整个数组的首地址。因为传参时是传值,所以这两个没区别)。所以在子函数内部,传进来的数组名就等于是一个指向数组首元素首地址的指针。所以sizeof得到的是8(64位系统).

(2)在子函数内传参得到的数组首元素的首地址,和外面得到的数组首元素直地址的值是相同的很多人把这种特性叫做“传址调用"(所谓的传址调用就是调用子函数时传了地址(也就是指针))此时可以通过传进去的地址来访问实参.

(3)数组作为函数形参时,[ ]里的数字是可有可无的 为什么?因为数组名做形参传递的实际只是个指针,根本没有数组长度这个信息.

(3)指针作为函数形参

(1)只有一句话:和数组作为函数形参是一样的,这就好像指针方式访问数组元素和数组方式访问数组元素的结果一样是一样的.

(4)结构体变量作为函数形参

(1)结构体变量作为函数形参的时候,实际上和普通变量(类似int之类的)传参时表现是一模一样的。所以说结构体变量其实也是普通变量而已.

(2)因为结构体一般都很太,所以如果直接用结构体变量进行传参,那么函数调用效率就会很低(因为在函数传参的时候需要将实参赋值给形参,所以当传参的变量越太调围效率就会越低).怎么解法?思路只有一个那就是不要传变量,改传变量的指针(地址)进去.

(3)结构体因为自身太大,所以传参应该用指针来传(但是程序员可以自己决定,你非要传结构体变量过去C语言也是允许的,只是效率低了) ;回想一下数组,为什么c语言设计的时候数组传参默认是传的数组首元素首地址而不是整个数组?

(5)传值调用与传址调用

(1)传值调用描述的是这样一种现象: x和y作为实参, 自己并没有真身进入swap1函数内部,而只是拷贝了一份自己的副本(副本具有和自己一样的值,但是是不同的变量)进入子函数swap1,然后我们在子函数swap1中交换的实际是副本而不是x、 y真身。所以在swap1内部确实是交换了,但是到外部的x和y根本没有受影响。

(2)在swap2中x和y真的被改变了(但是x和y真身还是没有进入swap2函数内,而是swap2函数内部跑出来把外面的x和y真身改了)。实际上实参x和y永远无法真身进入子函数内部(进去的只能是一份拷贝) ,但是在swap2我们把x和y的地址传进去给子函数了,于是乎在子函数内可以通过指针解引用方式从函数内部访问到外部的x和y真身,从而改变x和y。

(3)结论:这个世界上根本没有传值和传址这两种方式, c语言本身函数调用时一直是传值的,只不过传的值可以是变量名,也可以是变量的指针。

  • code
#include <stdio.h>

struct A
{
	char a;     //结构体变量对齐问题
	int b;      //因为要对齐存放,所以是12
	short c;
};

void swap1 (int a, int b)
{
	int temp;
	temp = a;
	a = b;
	b = temp;
	printf("in swap1,a = %d.\n", a);
	printf("in swap1,b = %d.\n", b);
}
void swap2 (int *a, int *b)
{
	int temp;
	temp = *a;
	*a = *b;
	*b = temp;
	printf("in swap1,*a = %d.\n", *a);
	printf("in swap1,*b = %d.\n", *b);
}



// 和指针传参一样(如func3)
void func5(struct A *a1)
{
	printf("sizeof(a1) = %d.\n", sizeof(a1));
	printf("a1 = %p.\n", a1);
	printf("a->a = %d.\n", a1->a);
	printf("a->b = %d.\n", a1->b);
}
// 结构体数据类型传参和普通变量传参一样 (如func1)
void func4(struct A a1)
{
	printf("sizeof(a1) = %d.\n", sizeof(a1));
	printf("&a1 = %p.\n", &a1);
	printf("a.a = %d.\n", a(1)a);
	printf("a.b = %d.\n", a1.b);
}

// 指针作为形参传参和数组是一样的(和func2 一样)
void func3(int *a)
{
	
	printf("sizeof(a) = %d.\n", sizeof(a));
	printf("in func3, a = %p.\n", a);
	
}
void func2(int a[])
{
	
	printf("sizeof(a) = %d.\n", sizeof(a));
	printf("in func2, a = %p.\n", a);
	
}

// &a 和 &b不同说明a和不是同一个变量(在内存中a和b是独立的2个内存空间)
// 但是a和b是有关联的,实际上b是a赋值得到的。
void func1(int b)
{
	printf("in func, &a = %p.\n", &b);
}


int main (void)
{
	
	
/*
	int x = 3;
	int y = 5;
	swap1(x, y);
	printf("x = %d , y = %d.\n", x, y);
	swap2(&x, &y);
	printf("x = %d , y = %d.\n", x, y);
*/

/*
	struct A a = 
	{
		.a = 5,     //注意这里是逗号隔开 不是分号
		.b = 6,
	};
	printf("sizeof(a1) = %d.\n", sizeof(a));
	printf("&a = %p.\n", &a);
	printf("a.a = %d.\n", a.a);
	printf("a.b = %d.\n", a.b);
	func5(&a);
*/

/*
	struct A a = 
	{
		.a = 5,
		.b = 6,
	};
	printf("sizeof(a) = %d.\n", sizeof(a));
	printf("&a = %p.\n", &a);
	printf("a.a = %d.\n", a.a);
	printf("a.b = %d.\n", a.b);
	func4(a);
*/
	
/*
	int a[5];
	printf("a = %p.\n", a);
	func3(a);
*/

/*	
	int a[5];
	printf("a = %p.\n", a);
	func2(a);
*/


/*
	int a = 5;
	printf("&a = %p.\n", &a);
	func1(a);
*/
	
	
	return 0;
}

(八)输入型参数与输出型参数

(1)函数为什么需要形参与返回值

(1)函数名是一个符号,表示整个函数代码段的首地址,实质是一个指针量,所以在程序中使用到函数名时都是当地址用的,用来调用这个函数的。

(2)函数体是函数的关键,由一对(括起来,包含很多句代码,函数体就是函数实际做的工作。

(3)形参列表和返回值。形参是函数的输入部分,返回值是函数的输出部分 对函数最好的理解就是把函数看成是一个加工机器(程序其实就是数据加工器),形参列表就是这个机器的原材料输入端:而返回值就是机器的成品输出端。

(4)其实如果没有形参列表和返回值,函数也能对数据进行加工,用全局变量即可。用全局变量来传参和用函数参数列表返回值来传参各有特点,在实践中都有使用。总的来说,函数参数传参用的比较多,因为这样可以实现模块化编程,而c语言中也是尽量减少使用全局变量。

(5)全局变量传参最大的好处就是省略了函数传参的开销,所以效率要高一些,但是实战中用的最多的还是传参,如果参数很多传参开销非常大,通常的做法是把很多参数打包成一个结构体,然后传结构体变量指针进去。

(2)函数传参中使用const指针

(1) const一般用在函数参数列表中,用法是const int *p;(意义是指针变量p本身可变的,而p所指向的变量是不可变的).

(2) const用来修饰指针做函数传参,作用就在于声明在函数内部不会改变这个指针所指向的内容,所以给该函数传一个不可改变的指针(char *p="linux";这种)不会触发错误:而一个未声明为const的指针的函数,你给他传一个不可更改的指针的时候就要小心了.

(3)函数需要向外部返回多个值时怎么办?

(1)一般来说,函数的收入部分就是函数参数,输出部分就是返回值。问题是函数的参数可以有很多个,而返回值只能有1个。这就造成我们无法让一个函数返回多个值。

(2)现实编程中,一个函数需要返回多个值是非常普遍的,因此完全依赖于返回值是不靠谱的,通常的做法是用参数来做返回(在典型的linux风格函数中,返回值是不用来返回结果的,而是用来返回0或者负数用来表示程序执行结果是对还是错,是成功还是失败)

(3)普遍做法,编程中函数的输入和输出都是靠函数参数的,返回值只是用来表示函数执行的结果是对(成功)还是错(失败)如果这个参数是用来做输入的,就叫输入型参数;如果这个参数的且的是用来做输出的,就叫输出型参数。

(4)输出型参数就是用来让函数内部把数据输出到函数外部的。

(4)总结

(1)看到一个函数的原型后,怎么样一眼看出来哪个参数做输入哪个做输出?函数传参如果传的是普通变量(不是指针)那肯定是输入型参数;如果传指针就有2种可能性了,为了区别,经常的做法是:如果这个参数是做输入的(通常做输入的在函数内部只需要读取这个参数而不会需要更改它)就在指针前面加const来修饰;如果函数形参是指针变量并且还没加const,那么就表示这个参数是用来做输出型参数的。

code

#include <stdio.h>

int multiple_1(int a);
void multiple_2(void);
void func1(int *p);
//void func2(const int *p);
void func3(char *p);
int multiple_3(int a, int *p);

int x, y;

int main (void)
{
	int a, b = 0, ret = -1;
	a = 30;
	ret = multiple_3(a, &b);
	if(ret == -1)
	{
		printf("出错了。\n");
	}
	else
	{
		printf("result = %d.\n", b);
	}
	
/*
	//char *pStr = "linux"; //Segmentation fault (core dumped) 值不能被该改变
    //数组形字符串存放在全局数据区或栈区,可读可写。指针字符串存放在常量区,只读不能写。
	char pStr[] = "linux";
	func3(pStr);
	printf("%s.\n", pStr);
*/
	
/*
	int a = 1;
	func1(&a);
//	func2(&a); //error: assignment of read-only location ‘*p’
*/

/*
	// 程序要完成的功能是:对一个数乘以5
	// 第一种方法:函数传参
	int a = 3;
	int b;
	b = multiple_1(a);
	printf("result = %d.\n", b);
	
	//第二种方法:用全局变量来传参
	x = 2;
	multiple_2();
	printf("result = %d.\n", y);
*/	

	
	return 0;
}
int multiple_3(int a, int *p)
{
	int tmp;
	
	tmp = 5 * a;
	if(tmp > 100)
	{
		return -1;
	}
	else
	{
		*p = tmp;
		return 0;
	}
}

void func3(char *p)
{
	*p = 'a';
	
}
/*
void func2(const int *p)
{
   *p = 5;
}
*/
void func1(int *p)
{
   *p = 5;
}

int multiple_1(int a)
{
	return a * 5;
}

void multiple_2(void)
{
	y = 5 * x;
}

2020.2.19

发布了21 篇原创文章 · 获赞 6 · 访问量 419

猜你喜欢

转载自blog.csdn.net/weixin_44112805/article/details/105032491