C语言中取地址符&做函数形参?—— 引用的讨论

本文深入探讨了C++中引用参数的使用原理及其与指针的区别。通过对比值传递、指针传递和引用传递的不同,解释了引用参数如何提高代码效率,并通过具体的代码示例和汇编指令分析来说明引用参数的底层实现。

转自:http://blog.youkuaiyun.com/JayRoxis/article/details/73060770

取地址符&做函数形参?

C语言强大且危险


引入

这个问题花去了整整一天的研究。

先看一段严蔚敏的《数据结构》中栈的例程:

栈的压入

这里面的&S第一遍看的时候想当然的认为是取了SqStack结构体的S的地址,没有细想。然后又看到了这句。

// platform: VC++6.0
Status Pop(SqStack &S, SElemType &e); //取地址符?
   
  • 1
  • 2

我开始突然发现,这真的是取地址符吗,对照了我自己写的程序,仔细推敲发现不太对。 
仔细看这里的&e,如果这是个整型的栈,那么SElemType就是int,那么这里就等于:

Status Pop(SqStack &S, int &e); //很奇怪
   
  • 1

类比的疑问

我们都知道:

int *a,*b; /* 定义了两个整型的指针 */
int **a, **b; /* 定义了整型指针的指针 */
   
  • 1
  • 2

那么难道说是

int &e; // 定义了以一个整型数为地址的变量e?
   
  • 1

仔细看下接下来的函数定义:

这里写图片描述

显然这里可以看出由于top指针指向的是SElemType类型,所以e是SElemType类型的。所以以上类比显然是不对的。


C/C++中的引用参数

查找了很多的资料发现,这个实际上是C++里的形参符号,必须要在跟在数据类型的后面使用。在函数内部对形参的操作都等同于直接操作原变量。

先说形参和实参

学过C语言的都知道,一个经典的例子是关于写一个交换两个变量a,b的值的函数:

// “形参不等于实参”的经典错误示范
void swap(int a, int b)
{
    int temp;
    temp = a;
    a = b;
    b = temp;
}

void main()
{
    int a = 1, b = 2;
    swap(a,b)l
}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

我们都知道把a,b作为形参传入时,会临时的分配形参空间读取实参a,b的值存入,这里的形参a,b实际地址是不同于原来的实参。

形象的说,实参a是一份讲义,你在调用函数的时候,函数就像学生一样去要讲义(传递的实参)。函数向系统要了张白纸(栈区空间),然后把这篇文章抄了一份拿去用了,取名也叫作a。然后他怎么修改都不会

继续准确点说, 在程序运行的时候会分配一个全局区,我们这里说的a,b实际上属于全局变量,存储在全局区,也有的地方叫做静态区。而这里的形参存储在栈区,仅仅是保存了全局量的值,所以所有对形参a,b的操作都和静态区的a,b无关。

这里实参传递给形参的过程叫做值传递。

附:C/C++程序的内存分配知识

一个由C/C++编译的程序占用的内存分为以下几个部分 :

1、栈区(stack)― 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。 
2、堆区(heap) ― 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。这个空间是公用的,如果没有释放会使得可用堆区空间变小,最好在申请后手动释放。 
3、全局区(静态区)(static)―,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后由系统释放 
4、文字常量区 ―常量字符串就是放在这里的。 程序结束后由系统释放 
5、程序代码区―存放函数体的二进制代码。 
所以我们可以理解为,这里的&e是为了说明e变量不是仅仅的把值传递进了函数内部。

那怎么通过函数操作函数外部的参数呢? 
根据C语言学习中标准解法,一是将实参的地址传递进函数中函数中,通过地址直接操作原变量的值;二是利用函数本身的返回

// 利用指针的经典解法
void swap(int *a, int *b)
{
    int temp;
    temp = *a;
    *a = *b;
    *b = temp;
}

void main()
{
    int a = 1, b = 2;
    swap(&a,&b);
}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

必须先弄清楚各种地址

要理清引用参数的使用和原理,明白这个&符号和指针的区别,先必须搞清楚数据的地址。

在《操作系统》中,可以得知三种地址的区别:逻辑地址、线性地址和物理地址。 
关于这三者的区别可以看这里:

操作系统逻辑地址、线性地址和物理地址 
http://www.cnblogs.com/dirichlet/archive/2011/03/18/1987746.html 
或者这里 
http://blog.youkuaiyun.com/geekwill/article/details/52449855

这里用图简单通俗的说下,为了通俗易懂,严格上并不准确:

这里写图片描述

我们的程序在操作系统中运行的时候,会给我们的程序(进程)在内存中分配一些空间。为了方便说明,这里假设内存是16位地址(实际上32位地址支持4G内存),我们可以看到a的物理地址是0x23。

然后0x2300是什么呢,这个是进程数据段的首地址,一般我们习惯叫做程序运行的入口地址

像上面的图所示,我们通过&a把a的逻辑地址传递进了函数swap中,然后swap函数通过*a找到a的物理地址,这个是操作系统完成的,其中会经过一些过程,需要先变换为线性地址。

那么我们可以总结:

实际上在C语言中,使用&取地址符取出的是变量的[逻辑地址],就算用汇编进行操作也是一样。变量的物理地址只有操作系统知道,实际上逻辑地址和物理地址都是32位整数(32位机)。两个不同进程,就算逻辑地址一样,实际的物理地址也不同。

   
  • 1
  • 2

这里关于各种变量的内存地址相关可以参考:

C语言内存地址基础 
http://blog.jobbole.com/44845/

关于C语言的函数调用过程更加深度严谨(也更难懂)的知识,墙裂推荐这篇文章:

深入理解C语言的函数调用过程 
http://blog.chinaunix.net/uid-23069658-id-3981406.html

通过引用传递和通过指针传递?

之前的两个例子,分别用常规的值传递和指针的传递实现数据交换的过程看起来不同,其实都是差不多的。实质上都是值传递

第一个例子的执行过程:

这里写图片描述

第二个例子的执行过程:

这里写图片描述

可以看出实际上利用指针的方法也只是把a,b的逻辑地址作为一个整数通过值传递到形参里存储起来了,值传递的内容是a,b的逻辑地址。这两种方式都需要额外的开辟栈区的内存,而且指针操作是易错且不安全的。

下面是通过引用参数完成的交换过程。

// 引用参数实现交换
void swap(int &a, int &b){
    int temp;
    temp = a;
    a = b;
    b = temp;
}

// Using main to test
void main(){
    void swap(int&, int&);

    int a = 1, b = 2;
    swap(a,b);
    printf("%d %d\n",a,b);
}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

有些文章说道,通过引用的方式传递给函数的是变量的地址,这种方式叫做地址传递方式,还提到这是和“值传递”十分不同的方式。 
有些书说道:“引用实际上是取了个‘别名’” 
还有的书和文章说道引用是比通过指针传递更加高效的方式,因为不需要开辟新的内存空间用来拷贝实参的地址。 
真的吗?

引用实现原理的讨论

先讨论引用实现的系列文章,大佬们讲得比较透彻,而且论据丰富。

c++中的引用的使用原理以及使用实例 
http://blog.youkuaiyun.com/ztz0223/article/details/1639305 
C++ 引用 参数传递 机制【强烈推荐】 
http://blog.youkuaiyun.com/huqinweI987/article/details/50769096 
C++引用的本质与修改引用的方法 
http://blog.youkuaiyun.com/huqinweI987/article/details/24874403 
举例剖析C++中引用的本质及引用作函数参数的使用 
http://www.jb51.net/article/80911.htm

如果不想看干货长文的就看下下面的通俗简短讨论吧。

我们看下下面这段小程序:

int i = 0;
int &j = i;     // 设j为i的一个引用
j = 10;         // 将j试着改变为10
int *p = &j;    // 观察引用的取地址操作
   
  • 1
  • 2
  • 3
  • 4

汇编(伪汇编)解析如下:

;int i = 0;
mov dword ptr [i],0;    // i的内容置为0;

;int &j = i;
lea  eax, [i];          // 将i的地址放入eax寄存器
mov  dword ptr[j],eax;  // 将i的地址传入j的内容

; j = 10;
mov eax, dword ptr[j];  // 取j的内容(i的地址)放入eax
mov dword ptr [eax], 0Ah; // 将eax地址指向修改为10;

;int *p = &j;
mov eax,dword ptr [j]     // 将j的内容传给eax
mov dword ptr [p],eax     // 把eax内容传入p的内容
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

实际上,通过对汇编的分析可以看出:

  1. “引用是一个别名”的说法并不准确,实际上实现过程中引用也可以看成是一种指针实际上引用变量存储的就是引用对象的地址,也要占用内存空间(和指针占用大小不同),只不过C++的标准规定了引用初始化完毕之后,对引用的操作就等于是对实际对象的操作

  2. 虽然引用可以看做特殊的指针,对引用的操作会被编译器解释成对地址指向的目标的操作。但和*p这种取指针指向对象的方式不同,这种方式不会开辟临时空间存储指针指向的对象。如果指向对象很大,操作重复数很多,这个差异就会对性能有十分大的影响。

  3. 引用的本身值,即引用对象的地址不可以像指针变量一样修改,对引用的操作只会解释成对引用对象的操作,可以理解引用变量是一个静态的指针

对第2条的解释,关于指针操作拷贝副本和引用节省空间的详细解释可以看上面的文章—— C++ 引用 参数传递 机制【强烈推荐】

<think>我们正在讨论C语言函数参数中的`const char *str`,特别是`*`的作用。 用户的问题:我想理解C语言函数参数中指针符号(*)的含义 请问C语言函数参数const char *str的*的作用是什么 首先,我们需要明确,在`const char *str`这个声明中,`*`是一个指针声明符,它表示`str`是一个指针变量。 具体分解: 1. `const char`:这表示指向的数据是常量字符,即不能通过这个指针修改所指向的字符。 2. `*str`:这里的`*`表示`str`是一个指针,它存储的是一个地址,该地址指向一个字符(或多个字符组成的字符串的第一个字符)。 因此,`const char *str`整体含义是:`str`是一个指针,它指向一个(或多个)常量字符,即我们不能通过`str`来修改它指向的字符数据。 现在,重点解释`*`的作用: - 在函数参数中,`*`表示该参数是一个指针。指针变量存储的是内存地址,而不是实际的数据值。 - 使用指针作为函数参数的主要目的有: a) 允许函数访问和操作函数外部的数据(通过地址间接访问)。 b) 避免大数据的复制(例如,传递一个字符串,如果不用指针而用数组,则会复制整个数组,效率低下)。 c) 通过指针可以修改函数外部的数据(但是在这个例子中,由于有`const`修饰,所以不能修改指向的数据)。 在`const char *str`这个参数中: - 函数接收一个字符串(实际上是一个字符数组)的起始地址。 - 由于有`const`,函数内部不能修改这个字符串的内容,但可以通过指针来读取字符串中的字符。 举例说明: ```c #include <stdio.h> // 函数参数:str是一个指针,指向一个常量字符(即字符串的起始地址) void print_string(const char *str) { // 通过指针遍历字符串直到遇到空字符'\0' while (*str != '\0') { // 这里的*str是解引用操作,得到当前指向的字符 putchar(*str); // 输出当前字符 str++; // 指针移动到下一个字符(注意:指针可以移动,但指向的内容不能修改) } } int main() { const char *msg = "Hello, World!"; // msg是一个指向常量字符串的指针 print_string(msg); // 将msg的值(即字符串的地址)传递给函数 return 0; } ``` 解释: - 在函数调用时,`msg`的值(即字符串"Hello, World!"的首地址)被传递给函数`print_string`的形参`str`。 - 在函数内部,通过`*str`可以访问当前指针位置上的字符(但不能修改,因为是`const`)。 - 通过`str++`可以移动指针(改变指针本身的值,即指向下一个字符),这是允许的,因为指针变量本身不是常量(注意:`const`修饰的是`char`,即指针指向的内容,而不是指针本身)。 总结`*`的作用: 1. 在声明中:`*`用于声明一个指针变量。 2. 在使用中:`*`作为解引用运算符,用于访问指针所指向的内存位置的值。 在函数参数中,使用指针(带`*`)的主要优势是: - 高效:传递一个地址(通常4或8字节)而不是整个数据(例如,一个长字符串可能几百字节)。 - 共享:多个函数可以通过指针访问同一份数据。 注意:虽然`const char *str`中的`*`表示指针,但`const`的位置决定了什么不能修改。这里`const`在`char`前面,表示指向的数据是常量。而指针本身(即`str`)可以改变(如`str++`)。 对比: - `char *const str`:这表示指针本身是常量(不能改变指向),但指向的内容可以修改。 - `const char *const str`:指针不能改变指向,指向的内容也不能修改。 因此,在函数参数`const char *str`中,`*`的作用是声明一个指针参数,使得函数能够通过地址访问外部数据,同时通过`const`保证不会修改该数据。 引用[2]中的例子:`strtod`函数的第一个参数就是`const char *str`,它用于指向一个字符串,函数会从这个字符串中转换出浮点数,但不会修改原字符串。 引用[3]中提到,使用`const char *`可以返回存储在只读段(如字符串字面量)的字符串,因为这样的字符串在函数退出后仍然有效(而栈上的数组在函数退出后就无效了)。 根据用户提供的引用[1]:`get_string`函数接收一个`const char **output`,这是一个指向指针的指针,并且指向的指针是`const char*`类型。这里`*`的作用同样是指针,但有两级:第一级`*`表示`output`是一个指针,它指向一个`const char*`类型(即一个指向常量字符的指针)。所以,通过`*output`可以修改那个指针(即让那个指针指向新分配的内存区域)。 但用户的问题集中在`const char *str`中的`*`,所以我们主要解释这一级指针。 最后,生成相关问题: 1. 在C语言中,函数参数使用一级指针和二级指针有什么区别? 2. 如何理解C语言中的指针常量(如`char *const p`)和常量指针(如`const char *p`)? 3. 在函数参数中,什么情况下应该使用`const`修饰指针? 4. 函数参数传递指针与传递数组有何异同? 5. 为什么在C语言中字符串通常用`const char *`指针传递? 注意:在回答中,我们引用了用户提供的引用[2]和引用[3],以及引用[1]中的内容,所以要在适当位置添加引用标识。</think>### C语言函数参数中`const char *str`的`*`作用详解 在C语言中,函数参数`const char *str`中的`*`是指针声明符,它的核心作用是**声明`str`为一个指针变量**。下面通过分层解析帮助你理解: #### 1. **指针的本质** - `*`表示`str`存储的是**内存地址**而非实际数据 - 该地址指向内存中的某个字符(或字符串的首字符) - 内存访问示例: ```c void print_first_char(const char *str) { char c = *str; // *操作符解引用:获取地址处的实际值 printf("首字符: %c\n", c); } ``` 当调用`print_first_char("Hello")`时: 1. `str`存储字符串"Hello"的首地址(例如0x1000) 2. `*str`解引用获取地址0x1000处的值`'H'` #### 2. **`const`与`*`的交互关系** | 声明形式 | 指针可变性 | 指向内容可变性 | 特点 | |-------------------|------------|----------------|--------------------| | `const char *str` | ✓ | ✗ | 内容只读(常用) | | `char *const str` | ✗ | ✓ | 指针固定 | | `char const *str` | ✓ | ✗ | 同`const char*` | 在`const char *str`中: - `*`右侧的`const`修饰`char` → **内容不可变** - 指针本身`str`可以改变指向: ```c void traverse(const char *str) { while(*str) { // *str解引用检查当前字符 putchar(*str); // 读取内容(允许) str++; // 修改指针指向下一个地址(允许) } } ``` #### 3. **函数参数中使用`*`的关键优势** - **高效传递大数据**: 传递字符串时只需拷贝4/8字节地址(32/64位系统),而非整个字符串: ```c // 低效方式(拷贝整个数组) void bad_print(char str[1000]) { ... } // 高效方式(仅传地址) void good_print(const char *str) { ... } ``` - **共享内存数据**: 允许多个函数访问同一内存区域: ```c const char *global = "Shared Data"; void func1(const char *s) { printf("Func1: %c\n", *s); // 读取首字符 } void func2(const char *s) { printf("Func2: %s\n", s); // 读取整个字符串 } ``` - **兼容多种输入类型**: 可接受三种形式的参数: ```c void demo(const char *str) { ... } demo("字面量"); // 1. 字符串字面量[^3] char arr[] = "数组"; demo(arr); // 2. 字符数组 char *ptr = malloc(10); strcpy(ptr, "堆内存"); demo(ptr); // 3. 堆分配的内存 ``` #### 4. **与无`*`声明的本质区别** | 参数类型 | 内存操作 | 典型应用场景 | |-------------------|------------------------|----------------------| | `char str[]` | 创建数组副本(低效) | 需要修改局部副本时 | | `const char *str` | 直接访问原内存(高效) | 只读访问字符串数据 | 示例对比: ```c // 方案1:数组传参(发生内存拷贝) void array_func(char str[10]) { str[0] = 'A'; // 修改的是副本 } // 方案2:指针传参(直接操作原内存) void pointer_func(const char *str) { // *str = 'A'; // 禁止!const保护原数据 printf("%s", str); } ``` #### 5. **标准库中的实际应用** 参考`strtod()`函数原型[^2]: ```c double strtod(const char *str, char **endptr); ``` - 第一个参数`const char *str`: `*`确保以指针形式传递字符串,避免拷贝开销 - `const`保证函数内不会修改原始字符串 ### 关键总结 1. `*`声明`str`为**指针变量**,存储目标内存地址 2. `const char *`组合表示**通过指针访问但不可修改数据** 3. 指针传递的核心优势:**高效 + 内存共享** 4. 使用准则: - 只读访问 → `const char *` - 需要修改 → `char *` - 固定指针 → `char *const` > "指针是C语言的灵魂,理解`*`是掌握系统编程的关键。" —— Dennis Ritchie
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值