跟我学C++中级篇——取地址操作

一、取地址

在C/C++开发中,指针操作既是一个难点,同时也是一个无法绕开的知识点。一个对象的指针,可以说就是一个对象的地址。那么如何取得这个对象指针呢?或者说如何取得对象地址呢?在传统的开发中,开发者可以通过“&”运算符来获取对象的指针即地址。而在C++11中,开发者又发现了一个std::addressof也可以达到同样的功能。下面将对它们之间的区别和联系进行分析。

二、std::addressof

在C++11中提供了一个接口std::addressof,它的作用与“&”运算符的功能一样也是获取一个对象的实际的地址。看一下其定义:

//定义
template< class T >
T* addressof(T& arg);
//实现
template<typename _Tp>  _GLIBCXX_NODISCARD
inline _GLIBCXX17_CONSTEXPR _Tp* addressof(_Tp& __r) noexcept
{ return std::__addressof(__r); }
template<typename _Tp> inline _GLIBCXX_CONSTEXPR _Tp*
__addressof(_Tp& __r) _GLIBCXX_NOEXCEPT
{ return __builtin_addressof(__r); }

__builtin_addressof函数是gcc内置的取地址的函数,它不管&运算符是否被重载都会获取对象的地址。

三、源码

其源实现基于两种场景:

  1. 基于编译器内置函数
    这是非常广泛的一种方法,代码如下:
  template<typename _Tp>
    _GLIBCXX_NODISCARD
    inline _GLIBCXX17_CONSTEXPR _Tp*
    addressof(_Tp& __r) noexcept
    { return std::__addressof(__r); }
  template<typename _Tp>
    inline _GLIBCXX_CONSTEXPR _Tp*
    __addressof(_Tp& __r) _GLIBCXX_NOEXCEPT
    { return __builtin_addressof(__r); }

__builtin_addressof函数是gcc内置的取地址的函数,它不管&运算符是否被重载都会获取对象的地址。
2. 基于非编译器内置函数
简易实现如下:

template< class T >
T* addressof(T& arg) {
    return (T*)&(char&)arg;
}

正规的实现如下:

template<typename _Tp>
inline _Tp* __addressof(_Tp& __r) _GLIBCXX_NOEXCEPT {
   return reinterpret_cast<_Tp*>(&const_cast<char&>(reinterpret_cast<const volatile char&>(__r)));
}
template<typename _Tp>
inline _Tp* addressof(_Tp& __r) noexcept {
  return std::addressof(__r);
}

从简单实现其实可以更好的理解其实现的过程,先是强制转成字符类型的引用(char&),这样做的目的一是防止导致对&运算符的重载操作(char类型作为基础类型没有重载&);二是可以防止出现转换为其它类型(大于1字节)可能导致的内存对齐动作(地址就有可能变化)。然后取地址获取真实的地址即&(char&)中的外面的&(char没有重载&),再强制转成指定类型的指针。
后面的实现,则是为了安全和效率起见,所作的安全控制和内联(inline),说一下const volatile,一个变量既可以是常量又可以可变。这要从两个角度来理解,比如一个寄存器,对程序而言它是常量,不应该尝试去改变它。但对硬件而言,它可能被改变。它的目的是为了防止编译器优化,确保获取准确的地址。而上面的const_cast则去除刚刚增加的const volatile。其它就非常好理解了。

四、应用场景和限制

既然有&可以获取地址,为什么又要造出一个std::addressof。大家都知道,除了一些特定的运算符不可以重载的话,大多数的运算符是可以被重载的,其中就包含&运算符。那么问题来了,在被重载了&运算符的对象中如果还是用&来取地址的话,会是什么情况呢?看下面的例程:

#include <iostream>
#include <memory>
struct Demo {
  int *operator&() { return &b_; }
  int *getaddr() { return &a_; }
  int a_;
  int b_;
};
int main() {
  Demo d;
  std::cout << "d address:" << &d << ",d.a_ address:" << d.getaddr() << std::endl;
  std::cout << " addressof get addr:" << std::addressof(d) << std::endl;
}

即使不运行代码也可以看出来,直接使用&获取的地址一定是有问题的。所以通过上面的代码可以总结出来,&和std::addressof的不同主要在于对重载&的应用的不同。后者可以始终正确的获取对象的地址。或者可以这样理解,在没有重载&运算符的情况下,二者的功能是一致的,编译器会将其优化化相同的代码。不过,std::addressof不能用于不完整类型,且在C++17前不允许用于右值。
std::addressof理论是在可以排除前面的限制情况下都可以使用。但有几个典型的应用场景:

  1. 在模板编程中必须使用
  2. &重载的情况下
  3. 确保获取准确的地址需求中
  4. 在处理无法主动控制的库或类型时

&的优势就在于代码简洁,清晰明了。所以如何选择二者,就看开发者对实际代码的了解程度了。在非上面几个典型的应用场景下,可以大胆的使用。能省一点是一点嘛。

五、例程

看一个cppreference上的例子:

#include <iostream>
#include <memory>
 
template<class T>
struct Ptr {
   T* data;
   Ptr(T* arg) : data(arg) {}
   ~Ptr() {delete data;}
   T** operator&() { return &data; }
};
 
template<class T>
void f(Ptr<T>* p) {
    std::cout << "Ptr overload called with p = " << p << '\n';
}
 
void f(int** p) {
    std::cout << "int** overload called with p = " << p << '\n';
}
 
int main()
{
    Ptr<int> p(new int(42));
    f(&p);                 // calls int** overload
    f(std::addressof(p));  // calls Ptr<int>* overload
}

上面的代表显示的结果应该是一致的才对。很好理解,第一个打印虽然重载的&运算符返回二级指针,但实际调用也是一个二级指针的对象转换,再加上&本身的取地址,恰恰是一级指针。

六、总结

任何技术不可能都是面面俱到的,新技术的出现,有的是颠覆性的修改而有的则是完善性的修改。std::addressof其实可以理解为对&的一种完善,它把一些可能出现的漏洞以及在发现这些漏洞后可能导致的代码复杂性做了统一的处理。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值