5个有意思的stackoverflow问题总结之一

stackoverflow五个有意思的C++问题

第一个、引用和指针的区别

问题

就是提出引用和指针它们之间到底有啥区别呢?

经典回答

  1. 指针可以改变其绑定的变量,也可以不用初始化(不建议这么做,有危险),

    int x = 5;
    int y = 6;
    int *p;
    p = &x;
    p = &y;
    *p = 10;
    assert(x == 5);
    assert(y == 10);
    

    引用不可以,且必须初始化。

    int x = 5;
    int y = 6;
    int &r = x;
    
  2. 指针变量有自己的实际地址和所占空间的大小,x86 上一般是 32 位,但是引用是和它绑定的变量共享一个地址。

    int x = 0;
    int &r = x;
    int *p = &x;
    int *p2 = &r;
    assert(p == p2);
    
  3. 指针可以指向指针的指针,指针的指针的指针,甚至更多层的指针,但引用只能有一层。

    int x = 0;
    int y = 0;
    int *p = &x;
    int *q = &y;
    int **pp = &p;
    pp = &q; // *pp = q
    **pp = 4;
    assert(y == 4);
    assert(x == 0);
    
  4. 指针可以赋为 nullptr,但引用不能初始化为空。当然你也可以使用其他的方法(毕竟奇淫技巧多)来实现。

    int *p = nullptr;
    int &r = nullptr; // compiling error
    int &r = *p;  // likely no compiling error, especially if the nullptr is hidden behind a function call, yet it refers to a non-existent int at address 0
    
  5. 指针支持算术运算,比如一个指针数组,使用++就可以拿到下一个位置的指针,+4就可以拿到后面的第四个。

  6. 指针需要以*来取值,引用不用。指向结构体或类对象的指针,还可以以->来获取其内部的成员,引用则使用.

  7. 没有“引用数组”这种说法,只有“指针数组”。

  8. 常量引用可以绑定临时对象,也就是右值,指针不行,搞不好会段错误。

    const int &x = int(12); // legal C++
    int *y = &int(12); // illegal to dereference a temporary.
    
  9. 引用用于函数的参数和返回值,有的时候会很有用。比如参数const std::string& name,还有单例模式中的引用返回。

注意,C++ 标准并没有明确要求编译器该如何实现引用,但是基本上所有编译器在底层处理上都会把引用当作指针来处理。比如下面是一个引用,

int &ri = i;

如果未被编译器完全优化,那么它的底层实现其实就和指针一样,开辟一段内存,存放 i 的地址。可以参考,

另外附一些可能需要的链接,

第二个、C++ 中的关键字explicit意味着什么

问题

C++ 中的关键字explicit是什么意思?

经典回答

我们知道编译器是允许进行隐式转换(implicit conversion)的,就是说如果类 A 有一个只有一个参数的构造函数,那么是允许从这个参数对象隐式转换为 A 对象的,直接看个例子就明白了,

class Foo
{
public:
  // single parameter constructor, can be used as an implicit conversion
  Foo (int foo) : m_foo (foo)
  {
  }

  int GetFoo () { return m_foo; }

private:
  int m_foo;
};

下面是一个以Foo类型为参数的函数,

void DoBar (Foo foo)
{
  int i = foo.GetFoo();
}

下面是调用构造函数,进行隐式转换的例子,

int main ()
{
  DoBar(42);
}

实参42是一个整型,不是Foo类型的,但是它可以正常调用,这就是因为隐式转换。因为存在Foo (int foo)这个构造函数,所以可以从int隐式转换为Foo。同样的,如果你定义了这样的构造函数Foo (double foo),也是允许从double隐式转化为Foo的。

但是如果你现在在构造函数的前面加个关键字explicit,它的意思就是要告诉编译器,这个隐式转换不会再被允许了,当编译到DoBar(42)的时候就会报错,除非你显示调用,像这样DoBar(Foo(42))

只有当你有一个好的理由允许构造函数隐式转换,不然的话请把它们都声明为explicit,因为隐式转换容易导致错误,而这个错误往往不容易察觉。比如下面这个的例子,

一个类构造函数MyString(int size),它可以创建一个指定长度size的字符串,而你现在有一个函数print(const MyString&),当调用print(3)的时候(其实你是想调用print("3"),因为粗心少敲了双引号),按道理你期望得到的值是3,但是实际上得到的只是一个长度为 3 的字符串而已。

第三个、如何对一个位(bit)置 1、清零和取反

问题

如题,如何对一个位(bit)置 1、清零和取反?

回答

对特定位进行置 1(bit-set)

|操作符来位置 1,

number |= 1UL << n;

这段代码是将number的第n位赋为 1。

注意,如果number的大小大于unsigned long,就需要把1UL换成1ULL

对特定位进行置置 0(bit-clear)

&操作符来清零一个位,

number &= ~(1UL << n);

number的第n位赋为 0。

对特定位进行置置反(bit-toggle)

^操作符来置反一个位(即 0 变 1,1 变 0),

number ^= 1UL << n;

number的第n位置反。

位检查(bit-check)

还补充以下一些位操作

bit = (number >> n) & 1U;

先将number右移 n 位,然后和 1 进行与操作,得到的值赋给变量 bit。如果第 n 位是 1,那么 bit 也会变为 1;如果是 0,bit 也会是 0。

根据另一个变量来置位
number ^= (-x ^ number) & (1UL << n);

如果 x 等于 1,就把 number 的第 n 位置为 1;如果 x 等于 0,就把 number 的第 n 位置为 0。

注意,如果 x 等于其它数(非 0 非 1),上面的式子结果就未知了。当然你可以使用逻辑运算符!来把 x 置 0 或 置1(也就是布尔化)。

unsigned long newbit = !!x;    // Also booleanize to force 0 or 1
number ^= (-newbit ^ number) & (1UL << n);

译注:原文中还有一段没有翻译,因为我不知道怎么翻译,这里原文贴在这里(此处的代码我在 Visual Studio 2017 上报错:一元负运算符应用于无符号类型,结果仍为无符号类型)。

To make this independent of 2’s complement negation behaviour (where -1 has all bits set, unlike on a 1’s complement or sign/magnitude >C++ implementation), use unsigned negation.

number ^= (-(unsigned long)x ^ number) & (1UL << n);

除了上面的方法,你还可以这么做,

number = (number & ~(1UL << n)) | (x << n);

(number & ~(1UL << n))会对第 n 位清零(clear),(x << n)左移 n 位,也就是赋值第 n 位为 0 或 1。同样的,只有 x 等于 0 或者 1 才会生效,如果是其它的数,结果未知。

另外,如果把这些操作封装在宏里,那么用起来就会显得清楚明白和简洁,可以参考 https://stackoverflow.com/questions/47981/how-do-you-set-clear-and-toggle-a-single-bit/263738#263738

第四、static_cast, dynamic_cast, const_cast 和 reinterpret_cast 怎么用

问题

下面这些类型转换的正确用法和应用场景是什么?

  • static_cast
  • dynamic_cast
  • const_cast
  • reinterpret_cast
  • C 语言风格类型转化(type)value
  • 函数式风格类型转换type(value)

经典回答

static_cast 是静态转换的意思,也就是在编译期间转换,转换失败的话会抛出一个编译错误。主要用于,

  1. 基本数据类型之间的转换。如把 int 转换成 char,把 int 转换成 enum。这种转换的安全性需要开发人员来保证。
  2. void 指针转换成目标类型的指针。这种转换的安全性需要开发人员来保证。
  3. 任何类型的表达式转换成 void 类型。
  4. 有转换构造函数或类型转换函数的类与其它类型之间的转换。例如 double 转 Complex(调用转换构造函数)、Complex 转 double(调用类型转换函数)。
  5. 类层次结构中基类和子类之间指针或引用的转换。进行上行转换(即子类的指针或引用转换成基类表示)是安全的,不过一般在进行这样的转化时会省略 static_cast;进行下行转换(即基类指针或引用转换成子类表示)时,由于没有动态类型检查,所以是不安全的,一般用 dynamic_cast 来替代。
class Complex{
public:
    Complex(double real = 0.0, double imag = 0.0): m_real(real), m_imag(imag){ }
public:
    operator double() const { return m_real; }  // 类型转换函数
private:
    double m_real;
    double m_imag;
};

int m = 100;
Complex c(12.5, 23.8);
long n = static_cast<long>(m);  // 宽转换,没有信息丢失
char ch = static_cast<char>(m);  // 窄转换,可能会丢失信息
int *p1 = static_cast<int*>(malloc(10 * sizeof(int)));  // 将 void 指针转换为具体类型指针
void *p2 = static_cast<void*>(p1);  // 将具体类型指针,转换为 void 指针
double real= static_cast<double>(c);  // 调用类型转换函数

dynamic_cast 是动态转换,会在运行期借助 RTTI 进行类型转换(这就要求基类必须包含虚函数),主要用于类层次间的下行转换(即基类指针或引用转换成子类表示)。对于指针,如果转换失败将返回 NULL;对于引用,如果转换失败将抛出 std::bad_cast 异常。

class Base { };
class Derived : public Base { };

Base a, *ptr_a;
Derived b, *ptr_b;

ptr_a = dynamic_cast<Base *>(&b);  // 成功
ptr_b = dynamic_cast<Derived *>(&a);  // 失败,因为基类无虚函数
class Base { virtual void dummy() {} };
class Derived : public Base { int a; };

Base *ptr_a = new Derived{};
Base *ptr_b = new Base{};

Derived *ptr_c = nullptr;
Derived *ptr_d = nullptr;

ptr_c = dynamic_cast<Derived *>(ptr_a);  // 成功
ptr_d = dynamic_cast<Derived *>(ptr_b);  // 失败,返回 NULL

// 检查下行转换是否成功
if (ptr_c != nullptr) {
	// ptr_a actually points to a Derived object
}

if (ptr_d != nullptr) {
    // ptr_b actually points to a Derived object
}

const_cast 主要用来修改类型的 const 或 volatile 属性。

int a = 5;
const int* pA = &a;
*pA = 10; // 编译错误,不允许修改 pA 指向的对象
int* pX = const_cast<int*>(pA); // 去掉 const 属性
*pX = 10 // 成功赋值

注意,如果你要修改的对象实际上是一个常量,这个转换就可能不会生效。

const int a = 5; // 常量
const int* pA = &a;
*pA = 10; // 编译错误,不允许修改 pA 指向的对象
int* pX = const_cast<int*>(pA); // 去掉 const 属性
*pX = 10 // 是否会真正地修改结果未知,因为对于 a 来说,编译器一般在编译的时候会把它放进常量表中

reinterpret_cast 是重新解释的意思,顾名思义,reinterpret_cast 这种转换仅仅是对二进制位的重新解释,不会借助已有的转换规则对数据进行调整,非常简单粗暴,所以风险很高。

reinterpret_cast 可以认为是 static_cast 的一种补充,一些 static_cast 不能完成的转换,就可以用 reinterpret_cast 来完成。例如两个具体类型指针之间的转换、int 和指针之间的转换(有些编译器只允许 int 转指针,不允许反过来)。

class A{
public:
    A(int a = 0, int b = 0): m_a(a), m_b(b){}
private:
    int m_a;
    int m_b;
};

// 将 char* 转换为 float*
char str[] = "reinterpret_cast";
float *p1 = reinterpret_cast<float*>(str);

// 将 int 转换为 int*
int *p = reinterpret_cast<int*>(100);

// 将 A* 转换为 int*
p = reinterpret_cast<int*>(new A(25, 96));

(type)valuetype(value) 其实是一个意思,只是写法风格的差异而已。它涵盖了上面四种*_cast的所有功能,同时它的使用需要完全由程序员自己把控。

参考

第五、在 C/C++ 中,#include < filename>和#include "filename"两种写法有什么区别

问题

如题所问,在 C/C++ 中,#include <filename>#include "filename"两种写法有什么区别?

经典回答

<filename>一般会去系统路径和编译器预指定的路径找。比如 Windows 系统库的#include <Windows.h>,Linux 系统库的#include <sys/socket.h>,C/C++ 编译器已预指定的的标准库#include <stdio.h>。GCC 命令中-I会给编译器另自指定一条搜寻路径,对于该路径下的文件,也会用<>包含。

"filename"一般会去工程目录下找,如果你的工程下有一个文件~/MyProject/src/widget.h里包含了#include "simple_dialog.h",那么它会去~/MyProject/src/下去找,找不到再依照<>查找的路径去找。

总的来说,

  • 系统库、标准库、编译器指定的路径(比如 GCC 的-I命令),都以#include <>来包含文件。
  • 程序员自己创建的工程文件,都以#include ""来包含。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cpp编程小茶馆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值