问题
就是提出引用和指针它们之间到底有啥区别呢?
经典回答
- 指针可以改变其绑定的变量,也可以不用初始化(不建议这么做,有危险),
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;
- 指针变量有自己的实际地址和所占空间的大小,x86 上一般是 32 位,但是引用是和它绑定的变量共享一个地址。
int x = 0;
int &r = x;
int *p = &x;
int *p2 = &r;
assert(p == p2);
- 指针可以指向指针的指针,指针的指针的指针,甚至更多层的指针,但引用只能有一层。
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);
- 指针可以赋为 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
- 指针支持算术运算,比如一个指针数组,使用
++
就可以拿到下一个位置的指针,+4
就可以拿到后面的第四个。 - 指针需要以
*
来取值,引用不用。指向结构体或类对象的指针,还可以以->
来获取其内部的成员,引用则使用.
。 - 没有“引用数组”这种说法,只有“指针数组”。
- 常量引用可以绑定临时对象,也就是右值,指针不行,搞不好会段错误。
const int &x = int(12); // legal C++
int *y = &int(12); // illegal to dereference a temporary.
- 引用用于函数的参数和返回值,有的时候会很有用。比如参数
const std::string& name
,还有单例模式中的引用返回。
注意,C++ 标准并没有明确要求编译器该如何实现引用,但是基本上所有编译器在底层处理上都会把引用当作指针来处理。比如下面是一个引用,
int &ri = i;
如果未被编译器完全优化,那么它的底层实现其实就和指针一样,开辟一段内存,存放 i 的地址。可以参考,
另外附一些可能需要的链接,
- My all-time favorite C++ FAQ lite
- References vs. Pointers
- An Introduction to References
- References and const
第二个、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
是静态转换的意思,也就是在编译期间转换,转换失败的话会抛出一个编译错误。主要用于,
- 基本数据类型之间的转换。如把 int 转换成 char,把 int 转换成 enum。这种转换的安全性需要开发人员来保证。
- void 指针转换成目标类型的指针。这种转换的安全性需要开发人员来保证。
- 任何类型的表达式转换成 void 类型。
- 有转换构造函数或类型转换函数的类与其它类型之间的转换。例如 double 转 Complex(调用转换构造函数)、Complex 转 double(调用类型转换函数)。
- 类层次结构中基类和子类之间指针或引用的转换。进行上行转换(即子类的指针或引用转换成基类表示)是安全的,不过一般在进行这样的转化时会省略 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;