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

相关文章

Nginx实现动态封禁IP的步骤指南

《Nginx实现动态封禁IP的步骤指南》在日常的生产环境中,网站可能会遭遇恶意请求、DDoS攻击或其他有害的访问行为,为了应对这些情况,动态封禁IP是一项十分重要的安全策略,本篇博客将介绍如何通过NG... 目录1、简述2、实现方式3、使用 fail2ban 动态封禁3.1 安装 fail2ban3.2 配

C++一个数组赋值给另一个数组方式

《C++一个数组赋值给另一个数组方式》文章介绍了三种在C++中将一个数组赋值给另一个数组的方法:使用循环逐个元素赋值、使用标准库函数std::copy或std::memcpy以及使用标准库容器,每种方... 目录C++一个数组赋值给另一个数组循环遍历赋值使用标准库中的函数 std::copy 或 std::

Java中String字符串使用避坑指南

《Java中String字符串使用避坑指南》Java中的String字符串是我们日常编程中用得最多的类之一,看似简单的String使用,却隐藏着不少“坑”,如果不注意,可能会导致性能问题、意外的错误容... 目录8个避坑点如下:1. 字符串的不可变性:每次修改都创建新对象2. 使用 == 比较字符串,陷阱满

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本

C++初始化数组的几种常见方法(简单易懂)

《C++初始化数组的几种常见方法(简单易懂)》本文介绍了C++中数组的初始化方法,包括一维数组和二维数组的初始化,以及用new动态初始化数组,在C++11及以上版本中,还提供了使用std::array... 目录1、初始化一维数组1.1、使用列表初始化(推荐方式)1.2、初始化部分列表1.3、使用std::

C++ Primer 多维数组的使用

《C++Primer多维数组的使用》本文主要介绍了多维数组在C++语言中的定义、初始化、下标引用以及使用范围for语句处理多维数组的方法,具有一定的参考价值,感兴趣的可以了解一下... 目录多维数组多维数组的初始化多维数组的下标引用使用范围for语句处理多维数组指针和多维数组多维数组严格来说,C++语言没

使用 sql-research-assistant进行 SQL 数据库研究的实战指南(代码实现演示)

《使用sql-research-assistant进行SQL数据库研究的实战指南(代码实现演示)》本文介绍了sql-research-assistant工具,该工具基于LangChain框架,集... 目录技术背景介绍核心原理解析代码实现演示安装和配置项目集成LangSmith 配置(可选)启动服务应用场景

SQL Server数据库迁移到MySQL的完整指南

《SQLServer数据库迁移到MySQL的完整指南》在企业应用开发中,数据库迁移是一个常见的需求,随着业务的发展,企业可能会从SQLServer转向MySQL,原因可能是成本、性能、跨平台兼容性等... 目录一、迁移前的准备工作1.1 确定迁移范围1.2 评估兼容性1.3 备份数据二、迁移工具的选择2.1

在 Windows 上安装 DeepSeek 的完整指南(最新推荐)

《在Windows上安装DeepSeek的完整指南(最新推荐)》在Windows上安装DeepSeek的完整指南,包括下载和安装Ollama、下载DeepSeekRXNUMX模型、运行Deep... 目录在www.chinasem.cn Windows 上安装 DeepSeek 的完整指南步骤 1:下载并安装