C进阶指南(2):数组和指针、打桩

2024-04-20 12:32

本文主要是介绍C进阶指南(2):数组和指针、打桩,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

原文地址:http://pfacka.binaryparadise.com/articles/guide-to-advanced-programming-in-C.html

翻译地址:http://blog.jobbole.com/72830/

三、指针和数组

尽管在某些上下文中数组和指针可相互替换,但在编译器看来二者完全不同,并且在运行时所表达的含义也不同。

当我们说对象或表达式有类型的时候,我们通常想的是定位器值的类型,也叫做左值。当左值有完全non-const类型时,此类型不是数组类型(因为数组本质是内存的一部分,是个只读常量,译者注),我们称此左值为可修改左值,并且此变量是个值,当表达式放到赋值运算符左边的时候,它被赋值。若表达式在赋值运算符的右边,此变量不必被修改,变量成为了修改左值的的内容。若表达式有数组类型,则此表达式的值是个指向数组第一个元素的指针。

上文描述了大多数场景下数组如何转为指针。在两种情形下,数组的值类型不被转换:当用在一元运算符 &(取地址)或 sizeof 时。参见C99/C11标准 6.3.2.1小节:

除非它是sizeof或一元&运算符,再或者它是用于初始化数组的字符文本,否则有着“数组”的表达式被转换为“指向”的类型的指针,此指针指向数组对象的首个非左值元素。

由于数组没有可修改的左值,并且在绝大多数情况下,数组类型的表达式的值被转为指针,因此不可能用赋值运算符给数组变量赋值(即int a[10]; a = 1;是错的,译者注)。下面是一个小示例:

short a[] = {1,2,3};
short *pa;
short (*px)[];void init(){pa = a;px = &a;printf("a:%p; pa:%p; px:%p\n", a, pa, px);printf("a[1]:%i; pa[1]:%i (*px)[1]:%i\n", a[1], pa[1], (*px)[1]);
}


(译者注:%i能识别输入的八进制和十六进制)

a 是 int 型数组,pa 是指向 int 的指针,px 是个未完成的、指向数组的指针。a 赋值给 pa 前,它的值被转为一个指向数组开头的指针。右值表达式 &a 并非意味着指向 int,而是一个指针,指向int 型数组因为当使用一元符号&时右值不被转换为指针。

表达式 a[1] 中下标的使用等价于 *(a+1),且服从如同 pa[1] 的指针算术规则。但二者有一个重要区别。对于 a 是数组的情况,a 变量的实际内存地址用于获取指向第一个元素的指针。当对于 pa 是指针的情况,pa 的实际值并不用于定位。编译器必须注意到 a 和 pa见的类型区别,因此声明外部变量时,指明正确的类型很重要。

int a[];
int *pa;


但在另外的编译单元使用下述声明是不正确的,将毁坏代码:

extern int *a;
extern int pa[];


3.1 数组作为函数形数

某些类型数组变为指针的另一个场合在函数声明中。下述三个函数声明是等价的:

void sum(int data[10]) {}void sum(int data[]) {}void sum(int *data) {}


编译器应报告函数 sum 重定义相关错误,因为在编译器看来上述三个例子中的参数都是 int 型的。.

多维数组是有点棘手的话题。首先,虽然用了“多维”这个词,C并不完全支持多维数组。数组的数组可能是更准确的描述。

typedef int[4] vector;
vector m[2] = {{1,2,3,4}, {4,5,6,7}};
int n[2][4] = {{1,2,3,4}, {4,5,6,7}};


变量 m 是长度为2的 vector 类型,vector 是长为4的 int 型数组。除了存储的内存位置不同外,数组 n 与 m 是相同的。从内存的角度讲,两个数组都如同括号内展示的内容那样,排布在连续的内存区域。访问到的和声明的完全一致。

int *p = n[1];
int y = p[2];


通过使用下标符号 n[1],我们获取到了每个元素大小为4字节的整型数组。因为我们要定位数组的第二个元素, 其位置在多维数组中是数组开始偏移四倍的整型大小。我们知道,在这个表达式中整型数组被转为指向 int 的指针,然后存为 p。然后 p[2] 将访问之前表达式产生的数组中的第三个元素。上面代码中的 y 等价于下面代码中的 z:

int z = *(*(n+1)+2);


也等价于我们初学C时写的表达式:

int x = n[1][2];


当把上文中的二维数组作为参数传输时,第一“维”数组会转为指针,指向再次阵列的数组的第一个元素。因此不需要指明第一维。剩余的维度需要明确指出其长度。否则下标将不能正确工作。当我们能够随心所欲地使用下述表格中的任一形式来定义函数接受数组时,我们总是被强制显式地定义最里面的(即维度最低的)数组的维度。

void sum(int data[2][4]) {}void sum(int data[][4]) {}void sum(int (*data)[4]) {}


为绕过这一限制,可以转换数组为指针,然后计算所需元素的偏移。

void list(int *arr, int max_i, int max_j){int i,j;for(i=0; i<max_i; i++){for(j=0; j<max_j; j++){int x = arr[max_i*i+j];printf("%i, ", x);}printf("\n");}
}


另一种方法是main函数用以传输参数列表的方式。main函数接收二级指针而非二维数组。这种方法的缺陷是,必须建立不同的数据,或者转换为二级指针的形式。不过,好在它运行我们像以前一样使用下标符号,因为我们现在有了每个子数组的首地址。

int main(int argc, char **argv){int arr1[4] = {1,2,3,4};int arr2[4] = {5,6,7,8};int *arr[] = {arr1, arr2};list(arr, 2, 4);
}void list(int **arr, int max_i, int max_j){int i,j;for(i=0; i<max_i; i++){for(j=0; j<max_j; j++){int x = arr[i][j];printf("%i, ", x);}printf("\n");}
}


用字符串类型的话,初始化部分变得相当简单,因为它允许直接初始化指向字符串的指针。

const char *strings[] = {"one","two","three"
};


但这有个陷阱,字符串实例被转换成指针,用 sizeof 操作符时会返回指针大小,而不是整个字符串文本所占空间。另一个重要区别是,若直接用指针修改字符串内容,则此行为是未定义的。

假设你能使用变长数组,那就有了第三种传多维数组给函数的方法。使用前面定义的变量来指定最里面数组的维度,变量 arr 变为一个指针,指向未完成的int数组。

void list(int max_i, int max_j, int arr[][max_j]){/* ... */int x = arr[1][3];
}

此方法对更高维度的数组仍然有效,因为第一维总是被转换为指向数组的指针。类似的规则同样作用于函数指示器。若函数指示器不是 sizeof 或一元操作符 & 的参数,它的值是一个指向函数的指针。这就是我们传回调函数时不需要 & 操作符的原因。

static void catch_int(int no) {/* ... */
};int main(){signal(SIGINT, catch_int);/* ... */
}


四、打桩(Interpositioning)

打桩是一种用定制的函数替换链接库函数且不需重新编译的技术。甚至可用此技术替换系统调用(更确切地说,库函数包装系统调用)。可能的应用是沙盒、调试或性能优化库。为演示过程,此处给出一个简单库,以记录GNU/Linux中 malloc 调用次数。

/* _GNU_SOURCE is needed for RTLD_NEXT, GCC will not define it by default */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <stdint.h>
#include <inttypes.h>static uint32_t malloc_count = 0;
static uint64_t total = 0;void summary(){fprintf(stderr, "malloc called: %u times\n", count);fprintf(stderr, "total allocated memory: %" PRIu64 " bytes\n", total);
}void *malloc(size_t size){static void* (*real_malloc)(size_t) = NULL;void *ptr = 0;if(real_malloc == NULL){real_malloc = dlsym(RTLD_NEXT, "malloc");atexit(summary);}count++;total += size;return real_malloc(size);
}


打桩要在链接libc.so之前加载此库,这样我们的 malloc 实现就会在二进制文件执行时被链接。可通过设置 LD_PRELOAD 环境变量为我们想让链接器优先链接的全路径。这也能确保其他动态链接库的调用最终使用我们的 malloc 实现。因为我们的目标只是记录调用次数,不是真正地实现内存分配,所以我们仍需要调用“真正”的 malloc 。通过传递 RTLD_NEXT 伪处理程序到 dlsym,我们获得了指向下一个已加载的链接库中 malloc 事件的指针。第一次 malloc 调用 libc 的 malloc,当程序终止时,会调用由 atexit 注册的获取和 summary 函数。看GNU/Linxu中打桩行为(真的184次调用!):

$ gcc -shared -ldl -fPIC malloc_counter.c -o /tmp/libmcnt.so
$ export LD_PRELOAD="/tmp/libstr.so"
$ psPID TTY          TIME CMD2758 pts/2    00:00:00 bash4371 pts/2    00:00:00 ps
malloc called: 184 times
total allocated memory: 302599 bytes


4.1 符号可见性

默认情况下,所有的非静态函数可被导出,所有可能仅定义有着与其他动态链接库函数甚至模板文件相同特征标的函数,就可能在无意中插入其它名称空间。为防止意外打桩、污染导出的函数名称空间,有效的做法是把每个函数声明为静态的,此函数在目标文件之外不能被使用。

在共享库中,另一种控制导出的共享目标的方式是用编译器扩展。GCC 4.x和Clang都支持 visibility属性和 -fvisibility 编译命令来对每个目标文件设置全局规则。其中 default 意味着不修改可见性,hidden 对可见性的影响与 static 限定符相同。此符号不会被放入动态符号表,其他共享目标或可执行文件看不到此符号。

#if __GNUC__ >= 4 || __clang__#define EXPORT_SYMBOL __attribute__ ((visibility ("default")))#define LOCAL_SYMBOL  __attribute__ ((visibility ("hidden")))
#else#define EXPORT_SYMBOL#define LOCAL_SYMBOL
#endif


全局可见性由编译器参数指定,可通过设置 visibility 属性被本地覆盖。实际上,全局策略设置为 hidden,则所有符号会被默认为本地的,只有修饰 __attribute__ ((visibility (“default”))) 将被导出。


这篇关于C进阶指南(2):数组和指针、打桩的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/920267

相关文章

SpringBoot整合OpenFeign的完整指南

《SpringBoot整合OpenFeign的完整指南》OpenFeign是由Netflix开发的一个声明式Web服务客户端,它使得编写HTTP客户端变得更加简单,本文为大家介绍了SpringBoot... 目录什么是OpenFeign环境准备创建 Spring Boot 项目添加依赖启用 OpenFeig

SpringBoot请求参数接收控制指南分享

《SpringBoot请求参数接收控制指南分享》:本文主要介绍SpringBoot请求参数接收控制指南,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Spring Boot 请求参数接收控制指南1. 概述2. 有注解时参数接收方式对比3. 无注解时接收参数默认位置

CentOS7更改默认SSH端口与配置指南

《CentOS7更改默认SSH端口与配置指南》SSH是Linux服务器远程管理的核心工具,其默认监听端口为22,由于端口22众所周知,这也使得服务器容易受到自动化扫描和暴力破解攻击,本文将系统性地介绍... 目录引言为什么要更改 SSH 默认端口?步骤详解:如何更改 Centos 7 的 SSH 默认端口1

SpringBoot多数据源配置完整指南

《SpringBoot多数据源配置完整指南》在复杂的企业应用中,经常需要连接多个数据库,SpringBoot提供了灵活的多数据源配置方式,以下是详细的实现方案,需要的朋友可以参考下... 目录一、基础多数据源配置1. 添加依赖2. 配置多个数据源3. 配置数据源Bean二、JPA多数据源配置1. 配置主数据

Java数组初始化的五种方式

《Java数组初始化的五种方式》数组是Java中最基础且常用的数据结构之一,其初始化方式多样且各具特点,本文详细讲解Java数组初始化的五种方式,分析其适用场景、优劣势对比及注意事项,帮助避免常见陷阱... 目录1. 静态初始化:简洁但固定代码示例核心特点适用场景注意事项2. 动态初始化:灵活但需手动管理代

python中各种常见文件的读写操作与类型转换详细指南

《python中各种常见文件的读写操作与类型转换详细指南》这篇文章主要为大家详细介绍了python中各种常见文件(txt,xls,csv,sql,二进制文件)的读写操作与类型转换,感兴趣的小伙伴可以跟... 目录1.文件txt读写标准用法1.1写入文件1.2读取文件2. 二进制文件读取3. 大文件读取3.1

SpringBoot中配置Redis连接池的完整指南

《SpringBoot中配置Redis连接池的完整指南》这篇文章主要为大家详细介绍了SpringBoot中配置Redis连接池的完整指南,文中的示例代码讲解详细,具有一定的借鉴价值,感兴趣的小伙伴可以... 目录一、添加依赖二、配置 Redis 连接池三、测试 Redis 操作四、完整示例代码(一)pom.

Linux内核参数配置与验证详细指南

《Linux内核参数配置与验证详细指南》在Linux系统运维和性能优化中,内核参数(sysctl)的配置至关重要,本文主要来聊聊如何配置与验证这些Linux内核参数,希望对大家有一定的帮助... 目录1. 引言2. 内核参数的作用3. 如何设置内核参数3.1 临时设置(重启失效)3.2 永久设置(重启仍生效

Python列表去重的4种核心方法与实战指南详解

《Python列表去重的4种核心方法与实战指南详解》在Python开发中,处理列表数据时经常需要去除重复元素,本文将详细介绍4种最实用的列表去重方法,有需要的小伙伴可以根据自己的需要进行选择... 目录方法1:集合(set)去重法(最快速)方法2:顺序遍历法(保持顺序)方法3:副本删除法(原地修改)方法4:

go 指针接收者和值接收者的区别小结

《go指针接收者和值接收者的区别小结》在Go语言中,值接收者和指针接收者是方法定义中的两种接收者类型,本文主要介绍了go指针接收者和值接收者的区别小结,文中通过示例代码介绍的非常详细,需要的朋友们下... 目录go 指针接收者和值接收者的区别易错点辨析go 指针接收者和值接收者的区别指针接收者和值接收者的