从零开始学习嵌入式P12----指针(中)
从零开始学习嵌入式P12----指针中本篇继续学习一维字符数组传参、返回指针的函数、指向函数的指针以及const与指针的不同组合。示例默认在 Linux GCC 环境中运行。本篇目标学完这一篇我们应该能够正确把一维字符数组传给函数使用指针遍历以\0结尾的字符串区分字符串有效长度和字符数组容量使用const char *表达只读字符串参数理解“指针函数”就是返回值为指针的函数判断函数返回的指针在函数结束后是否仍然有效避免返回自动局部变量的地址理解“函数指针”是能够指向函数的指针正确定义、赋值和调用函数指针理解回调函数和函数指针表的基本用途区分“指向常量的指针”和“常量指针”正确阅读const int *p、int *const p和const int *const p在函数接口中使用const表达只读意图。一、一维字符数组传参对于chartext[32]hello;把字符数组传给函数时数组表达式通常转换为指向首元素的指针。因此下面两种函数形参写法等价voidprint_text(constchartext[]);voidprint_text(constchar*text);函数并没有得到一份完整数组副本而是通过指针访问调用者数组中的字符。调用者的字符数组 text | | 首元素地址 v 函数形参中的指针 | v text[0] text[1] text[2] ... \01. 使用下标访问#includestdio.hvoidprint_text(constchartext[]){for(intindex0;text[index]!\0;index){putchar(text[index]);}putchar(\n);}intmain(void){charmessage[32]hello;print_text(message);return0;}2. 使用指针遍历同一操作也可以使用指针完成#includestdio.hvoidprint_text(constchar*text){constchar*currenttext;while(*current!\0){putchar(*current);current;}putchar(\n);}这里current最开始指向第一个字符*current读取当前字符current移动到下一个字符遇到\0时结束。如果允许调用者传入空指针应先检查voidprint_text(constchar*text){if(textNULL){return;}while(*text!\0){putchar(*text);text;}putchar(\n);}形参text是指针值的副本因此在函数中执行text不会改变调用者的数组名或指针变量。二、字符数组参数的长度和容量字符数组传入函数后函数不能通过sizeof得到调用者数组的总容量voidprocess(chartext[]){/* sizeof(text) 得到的是指针大小不是调用者数组容量 */}根据数据含义常见接口有三种设计。1. 以\0结束的只读字符串voidprint_text(constchar*text);函数依靠\0判断字符串结束。调用者必须保证text指向有效内存可访问范围内存在\0函数执行期间字符串保持有效。2. 已知有效数据长度的字符序列voidsend_data(constchar*data,size_tlength);这种形式不要求数据一定以\0结尾适合串口数据、网络数据或二进制缓冲区。3. 可写缓冲区及其容量intreceive_text(char*buffer,size_tcapacity);如果函数需要向数组写入数据就必须知道可写容量避免越界。需要区分名称含义字符串长度\0之前的有效字符个数数据长度当前缓冲区中有效数据的字节数数组容量缓冲区一共能够容纳多少个元素这些数值可能不同函数接口应明确每个参数表示什么。三、可写字符数组与只读字符串如果函数需要修改调用者的字符数组形参可以使用voidreplace_first(char*text,charreplacement){if(textNULL||text[0]\0){return;}text[0]replacement;}调用charmessage[32]hello;replace_first(message,H);puts(message);输出Hello如果函数只读取字符不应修改应使用constchar*text字符数组与字符串字面量下面定义的是可修改数组charmessage[]hello;message[0]H;/* 正确 */下面定义的是指向字符串字面量的指针constchar*messagehello;字符串字面量不能被程序修改。使用const char *可以让编译器阻止通过该指针写入/* message[0] H; *//* 错误 */不要写成char *message hello后尝试修改内容即使某些编译器接受这种初始化修改字符串字面量仍会产生未定义行为。四、指针函数返回指针的函数“指针函数”首先是一个函数只是它的返回值类型为指针。基本形式指向类型*函数名(参数列表);例如int*find_value(int*array,size_tlength,inttarget);constchar*select_message(intsuccess);1. 如何阅读声明int*find_value(int*array,size_tlength,inttarget);先看函数名后面的圆括号find_value(...) 是一个函数再看前面的返回类型函数返回 int *所以它是“返回int *的函数”。2. 返回数组元素地址#includestddef.hint*find_value(int*array,size_tlength,inttarget){if(arrayNULL){returnNULL;}for(size_tindex0;indexlength;index){if(array[index]target){returnarray[index];}}returnNULL;}调用#includestdio.hintvalues[]{10,20,30,40,50};size_tlengthsizeof(values)/sizeof(values[0]);int*resultfind_value(values,length,30);if(result!NULL){printf(found %d\n,*result);}函数返回的是调用者数组中某个元素的地址。只要数组仍然存在而且没有越过有效范围这个返回指针就可以继续使用。3. 通过返回指针修改原对象因为result指向原数组元素所以可以写if(result!NULL){*result300;}这会把原数组中的30修改为300。如果调用者只应该读取结果函数应返回指向const的指针constint*find_value_readonly(constint*array,size_tlength,inttarget);五、函数不能返回自动局部变量的地址错误示例int*get_value(void){intlocal_value100;returnlocal_value;}local_value是自动局部变量。函数返回时它的生存周期结束返回的地址立即成为悬空指针。进入 get_value | v 创建 local_value | v 返回 local_value | v 函数结束local_value 生存周期结束 | v 调用者得到悬空指针即使测试时偶尔还能看到原值也不代表代码正确。使用这个指针会产生未定义行为。更准确的规则是函数不能返回指向已经结束生存周期对象的指针。六、哪些指针可以从函数返回返回指针之前必须确认它指向的对象在函数返回后仍然存在。1. 返回调用者提供的对象地址int*choose_larger(int*left,int*right){if(leftNULL){returnright;}if(rightNULL){returnleft;}return(*left*right)?left:right;}返回值指向调用者拥有的对象。调用者负责保证对象生存周期足够长。2. 返回全局对象地址staticintdevice_state0;int*get_device_state_address(void){returndevice_state;}全局对象在程序整个运行期间存在因此其地址在函数返回后仍然有效。不过直接暴露可写地址会让外部代码绕过模块控制。只需要读取时优先返回数值或const int *。3. 返回静态局部对象地址constint*get_counter(void){staticintcounter0;counter;returncounter;}静态局部变量的作用域在函数内但生存周期贯穿整个程序因此可以返回它的地址。需要注意多次调用得到的是同一个对象地址后一次调用可能改变前一次结果指向的内容并发、线程或中断环境中可能需要同步这种函数通常不可重入。4. 返回字符串字面量地址constchar*select_message(intsuccess){returnsuccess?success:failed;}字符串字面量具有静态存储期在程序运行期间存在。返回类型应使用const char *调用者不能修改其内容。5. 返回动态分配内存后续学习动态内存后函数也可以返回动态分配空间的地址。此时必须明确由谁负责释放避免内存泄漏和重复释放。在资源有限的嵌入式系统中动态内存需要谨慎使用。七、直接使用函数返回的指针返回的指针可以保存到变量constchar*messageselect_message(1);puts(message);也可以直接作为另一个函数的实参puts(select_message(1));但必须确认返回值满足下一个函数的要求。例如可能返回NULL的函数不能直接交给要求非空参数的接口int*resultfind_value(values,length,target);if(result!NULL){printf(%d\n,*result);}如果直接写成printf(%d\n,*find_value(values,length,target));当没有找到目标时会解引用空指针。因此短写法不能省略必要的错误检查。八、函数指针指向函数的指针“函数指针”首先是一个指针它保存某个兼容函数的地址。假设有函数intadd(intleft,intright){returnleftright;}可以定义一个能够指向这种函数的指针int(*operation)(int,int)add;这里operation是变量名(*operation)说明它是指针(int, int)说明它指向的函数接收两个int参数最左侧的int说明所指函数返回int。为什么必须有括号int(*operation)(int,int);是函数指针变量。而int*operation(int,int);是一个返回int *的函数也就是指针函数。括号决定了声明含义。九、给函数指针赋值函数名在多数表达式中会转换为指向该函数的指针int(*operation)(int,int)add;也可以显式取地址int(*operation)(int,int)add;两种写法效果相同。可以让同一个函数指针改为指向其他兼容函数intsubtract(intleft,intright){returnleft-right;}operationsubtract;函数类型必须兼容。下面的函数不能直接赋给前面的operationdoubledivide(doubleleft,doubleright);它的参数类型和返回类型都不同。不要通过强制类型转换掩盖不兼容的函数指针类型。使用错误类型调用函数会产生未定义行为。十、通过函数指针调用函数函数指针可以像函数名一样调用intresultoperation(10,20);也可以显式解引用intresult(*operation)(10,20);两种写法等价通常使用第一种更简洁。完整示例#includestdio.hintadd(intleft,intright){returnleftright;}intsubtract(intleft,intright){returnleft-right;}intmain(void){int(*operation)(int,int)add;printf(add %d\n,operation(20,5));operationsubtract;printf(subtract %d\n,operation(20,5));return0;}调用函数指针前应确认它不是空指针if(operation!NULL){intresultoperation(20,5);}十一、区分指针函数和函数指针名称示例本质指针函数int *find(int value);函数返回int *函数指针int (*operation)(int);指针指向返回int的函数阅读声明时可以先找变量名或函数名int*find(intvalue);find先与()结合所以find是函数。int(*operation)(intvalue);括号让operation先与*结合所以operation是指针。可以用一句话记忆指针函数返回指针的函数 函数指针指向函数的指针十二、const的基本含义const表示通过当前表达式不能修改被限定的对象。它更接近“只读接口约束”不应简单理解为数据在物理内存中永远不可改变。constintvalue100;通过变量名value不能修改该对象/* value 200; *//* 错误 */当const与指针一起使用时关键是判断它修饰的是指针指向的数据*pointer指针变量pointer两者同时被修饰。十三、指向常量的指针下面两种写法等价constint*pointer;intconst*pointer;可以理解为pointer 是一个可变指针 pointer 指向的 int 不能通过 pointer 修改示例intfirst100;intsecond200;constint*pointerfirst;可以改变指针的指向pointersecond;不能通过该指针修改对象/* *pointer 300; *//* 错误 */但如果原对象本身不是const仍然可以通过其他可写方式修改second300;printf(%d\n,*pointer);/* 读取到 300 */因此const int *表示“不能通过这个指针写”不代表底层对象一定永远不变。常见用途只读函数参数intsum_array(constint*array,size_tlength);voidprint_text(constchar*text);函数承诺不通过这些指针修改调用者数据。十四、常量指针int*constpointervalue;这里const修饰指针变量pointerpointer 保存的地址不能改变 pointer 指向的 int 可以通过 *pointer 修改示例intvalue100;intother200;int*constpointervalue;可以修改指向对象*pointer300;不能改变指针本身/* pointer other; *//* 错误 */常量指针必须在定义时获得初始值因为定义后不能再赋予另一个地址/* int *const pointer; *//* 没有可用初始化目标通常没有意义 */常量指针常用于表示一个对象在当前作用域内必须始终绑定到固定目标。十五、指向常量的常量指针下面两种写法等价constint*constpointervalue;intconst*constpointervalue;它同时限制pointer保存的地址不能改变不能通过pointer修改指向对象。intvalue100;intother200;constint*constpointervalue;/* pointer other; *//* 错误pointer 不能改变 *//* *pointer 300; *//* 错误不能通过 pointer 修改对象 */仍然可以读取printf(%d\n,*pointer);十六、四种指针声明对照假设都有合适的初始化声明指针能否改指向能否通过指针修改对象常用描述int *p可以可以普通指针const int *p可以不可以指向常量的指针int *const p不可以可以常量指针const int *const p不可以不可以指向常量的常量指针可以从变量名向左读声明constint*pointer;pointer是指针指向const int。int*constpointervalue;pointer是const指针指向int。constint*constpointervalue;pointer是const指针指向const int。const出现在*左边时通常限制指向对象出现在*右边、变量名左边时限制指针变量本身。十七、指针类型中的const转换普通可写指针可以转换为指向常量的指针intvalue100;int*writablevalue;constint*readonlywritable;这是安全的因为只是减少通过readonly能执行的操作。反方向不能直接转换constintvalue100;constint*readonlyvalue;/* int *writable readonly; *//* 丢弃 const 限定 */不要通过强制类型转换去掉const后修改真正定义为const的对象constintvalue100;int*pointer(int*)value;/* *pointer 200; *//* 未定义行为 */如果一个接口不需要修改数据就应该从一开始使用正确的const参数而不是在调用处反复转换。十八、const与字符数组接口只读取字符串voiddisplay_message(constchar*message);这种函数既可以接收可写字符数组charbuffer[]hello;display_message(buffer);也可以接收字符串字面量display_message(hello);需要修改字符数组voidnormalize_text(char*text);它只能接收指向可写字符存储区的指针charbuffer[]hello;normalize_text(buffer);不应把字符串字面量传给会修改内容的函数。返回只读字符串constchar*get_status_text(intstatus){returnstatus?ready:error;}返回const char *明确告诉调用者可以读取结果但不能修改字符串内容。十九、组合示例查找字符并返回只读指针下面的函数在字符串中查找某个字符并返回该字符位置。函数不修改字符串所以参数和返回值都使用const char *#includestddef.hconstchar*find_character(constchar*text,chartarget){if(textNULL){returnNULL;}while(*text!\0){if(*texttarget){returntext;}text;}returnNULL;}使用#includestdio.hconstcharmessage[]embedded;constchar*positionfind_character(message,b);if(position!NULL){printf(found: %c\n,*position);}这里同时体现了一维字符数组传参使用指针遍历字符串返回调用者数组内部的地址使用NULL表示未找到使用const保持只读约束。调用者必须保证message在使用position期间仍然有效。小结一维字符数组传给函数时会按首元素指针处理。以\0结尾的字符串可以通过下标或指针遍历但函数不能通过字符数组形参上的sizeof得到原数组容量。只读字符串参数应使用const char *可写缓冲区则应同时传入容量。指针函数是返回值为指针的函数。函数可以返回调用者对象、全局对象、静态对象或字符串字面量中的有效地址但不能返回已经结束生存周期的自动局部变量地址。任何可能为空的返回指针都必须在解引用前检查。函数指针是指向函数的指针。声明int (*operation)(int, int)表示operation能够指向接收两个int并返回int的函数。const int *p表示不能通过p修改所指对象但p可以改指向int *const p表示p不能改指向但可以通过它修改对象const int *const p同时限制两者。正确使用const可以表达函数接口的只读意图减少误修改并让模块边界更加清晰。