C++学习-入门到精通【19】杂项汇总
目录
一、const_cast运算符
const_cast
运算符可以强制去除const
和volatile
的限定,使得一个声明为const的变量可以被修改。注意,使用该运算符是一件危险的事,应该明确程序的确需要修改一个常变量的功能才使用它。该运算符并不会进行任何检查。
这里我们对比之前我们用到过的其他的一些强制转换运算符:static_cast
、dynamic_cast
和reinterpret_cast
。
static_cast
的工作是在编译时进行相关类型间的安全转换,同样也可以用于向下转换,但是安全必须由程序员来保证(也就是不进行运行时类型检查);
dynamic_cast
的核心工作是在运行时进行多态类型的安全向下转换。会进行运行时类型检查,被转换的对象是否与要转换的目标类型相同,如果相同继续转换,不同则返回一个空指针;
reinterpret_cast
进行低级别的比特位重新解释(最危险的转换),它的工作是进行任意指针间的转换,不会进行任何类型检查,且高度依赖于平台;
const_cast
则是用于添加或移除const/volatile
关键字;
下面给出一个使用const_cast的例子:
#include <iostream>
#include <cstring> // 包含C风格的字符串操作函数,比如strcmp,strcpy等等
#include <cctype> // 包含字符处理库,里面包含如isdigit之类的函数
using namespace std;
const char* maximum(const char* first, const char* second)
{
return ((strcmp(first, second) >= 0) ? first : second);
}
int main()
{
char s1[] = "hello";
char s2[] = "goodbye";
// 使用const_cast将一个const char*的类型转换成一个char*类型
char *maxPtr = const_cast<char*>(maximum(s1, s2));
cout << "The larger string is: " << maxPtr << endl;
for (size_t i = 0; i < strlen(maxPtr); ++i)
{
maxPtr[i] = toupper(maxPtr[i]);
}
cout << "The larger string capitalized is: " << maxPtr << endl;
}
运行结果:
二、mutable类成员
该关键字大多数是用来修饰一个类的数据成员的,表示它永远可以被修改,即使它是一个声明为const的对象的数据成员,或是在调用一个声明为const的函数内部也可以进行修改。
示例代码:
#include <iostream>
using namespace std;
class TestMutable
{
public:
TestMutable(int v = 0)
:value(v)
{
}
int getValue() const
{
return ++value;
}
private:
mutable int value;
};
int main()
{
const TestMutable test(99);
cout << "Initial value: " << test.getValue();
cout << "\nModified value: " << test.getValue() << endl;
}
运行结果:
三、命名空间
一个程序可能会包含很多在不同范围内定义的标识符。有时一个范围的变量会与另一个范围中定义的同名变量发生“重叠”,从而引起一个命名上的冲突。
C++标准试图使用命名空间来解决这一问题。每个命名空间都定义了一个放置标识符和变量的范围。在使用一个命名空间时,每个成员的名字前必须加上一个命名空间名和二元作用域分辨运算符::
来限定。例如:MyNameSpace::member;
。
或者使用using
指令。通常在那些会使用命名空间成员的文件的开始处使用这样的using语句。例如:using namespace MyNameSpace;
。
它表明命名空间MyNameSpace中的成员可以在这个文件中直接使用,而不用在每个成员前面添加命名空间名和作用域分辨运算符。
也可以以如下形式来使用using指令:
using std::cout;
该语句将一个名字加入到指令出现的范围中。当使用cout
时就不用在前面加上std::
。
使用语句using namespace std;
会将命名空间std
中的所有名字都带入到该指令所在的范围中。
注意并不是所有的命名空间都能保证唯一性,两个第三方提供的命名空间可能就存在同名的标识符。
定义一个命名空间
示例代码:
#include <iostream>
using namespace std;
int integer = 98; // 定义一个全局变量
// 使用关键字namespace创建一个命名空间
namespace Example
{
const double PI = 3.14159;
const double E = 2.71828;
int integer = 8; // 与全局变量integer重名
void printValues();
// 定义一个内层命名空间
namespace Inner
{
enum Years { FISCAL1 = 1900, FISCAL2, FISCAL3 };
}
}
// 定义一个匿名命名空间
namespace
{
double doubleInUnnamed = 88.88;
}
int main()
{
cout << "doubleInUnnamed = " << doubleInUnnamed;
cout << "\n(global) integer = " << integer;
cout << "\nPI = " << Example::PI << "\nE = " << Example::E
<< "\ninteger = " << Example::integer << "\nFISCAL3 = "
<< Example::Inner::FISCAL3 << endl;
Example::printValues();
}
void Example::printValues()
{
cout << "\nIn printValues:\ninteger = " << integer
<< "\nPI = " << PI << "\nE = " << E
<< "\n(global) integer = " << ::integer << "\nFISCAL3 = "
<< Inner::FISCAL3 << endl;
}
运行结果:
一个命名空间可以由关键字namespace
来进行定义。一个命名空间可以包含常量、变量、类、嵌套的命名空间、函数等等。
命名空间的定义必须出现在全局作用域中或是嵌套在其他命名空间之中。与类必须在同一个文件中完整连续定义不同,命名空间可以在不同的文件的命名空间中进行定义,编译器会自动将它们进行合并。对于标准库头文件,在这些文件的内部都包含一个名为std
的命名空间块,所以我们在使用标准库头文件中的标识符时,可以使用using namespace std;
来使用命名空间std中的名字,来减少冗余的作用域分辨运算符使用。
using指令不应该出现在头文件中
命名空间在使用很多类库的大规模应用程序中非常有用,所以在这种情况下,发生命名冲突的可能性就更大,在开发这种项目时,记得一定不要在头文件中使用using指令。否则会将相应的名字带入包含这个头文件的任何文件中,这就极其容易导致命名冲突发生,并产生微妙且难以发现的错误。
命名空间同样可以存在别名,例如:
namespace CPPHTP = CPlusPlusHowToProgram;
就是为命名空间CPlusPlusHowToProgram创建了一个命名空间别名CPPHTP。
四、指向类成员的指针
示例代码:
#include <iostream>
using namespace std;
// 创建一个Test类,
// 该类包含两个公有成员
// 一个成员函数,一个数据成员
class Test
{
public:
void func()
{
cout << "In func\n";
}
int value;
};
void arrowStar(Test*);
void dotStar(Test*);
int main()
{
Test test;
test.value = 8;
arrowStar(&test);
dotStar(&test);
}
void arrowStar(Test* testPtr)
{
// 声明一个函数指针,并使用该指针指向Test类中的成员函数func
// 因为指针指向的对象是类中的成员,所以要在 * 前面指明作用域
void (Test::*memberPtr)() = &Test::func;
// 使用函数指针来调用成员函数
// 要通过一个指针类成员的指针来访问类成员
// 必须通过一个类的对象或类对象指针来使用该指针,前者使用.操作符,后者使用->操作符
// *memberPtr可以看成是对一个指针的解引用,得到一个对象成员
// 然后一个类对象或类对象指针要访问一个类成员,必须使用.或->操作符
// 所以调用语句为obj.*memberPtr 或 objPtr->*memberPtr
(testPtr->*memberPtr)();
}
void dotStar(Test* testPtr)
{
int Test::*vPtr = &Test::value;
cout << (*testPtr).*vPtr << endl;
}
运行结果:
注意,上面对于类成员的解释中*memberPtr
实际上是一种错误的语法,仅仅是这样解释可能让人更容易理解。
与普通指针是一个内存中的绝对地址(虽然也不是,仅仅是对于该程序而言,它的位置可以说是绝对的)不同,指向类成员的指针是一个“偏移+访问规则”的语义标记,它必须与一个类对象组合才可以使用。
所以对一个类成员指针的使用.*
或->*
作为一个整体使用。
五、多重继承
在C++中一个可以从多个类派生出来,这种技术就被称为多重继承。
注意,在系统设计中要正确地使用多重继承必须格外小心,当单一的继承或组合可以解决问题时,不应该使用多重继承。
在使用多重继承时,最常见的一个问题就是,每个基类都可能包含了具有相同名字的数据成员或成员函数。这在编译时会导致二义性问题。
例如:
Base1.h
#pragma once
class Base1
{
public:
Base1(int parameterValue)
:value(parameterValue)
{
}
int getData() const
{
return value;
}
protected:
int value;
};
Base2.h
#pragma once
class Base2
{
public:
Base2(char characterData)
:letter(characterData)
{
}
char getData() const
{
return letter;
}
protected:
char letter;
};
Derived.h
#pragma once
#include "Base1.h"
#include "Base2.h"
#include <iostream>
class Derived : public Base1, public Base2
{
friend std::ostream& operator<<(std::ostream&, const Derived&);
public:
Derived(int, char, double);
double getReal() const;
private:
double real;
};
Derived.cpp
#include "Derived.h"
using namespace std;
Derived::Derived(int integer, char character, double double1)
:Base1(integer), Base2(character),real(double1)
{
}
double Derived::getReal() const
{
return real;
}
ostream& operator<<(ostream& output, const Derived& derived)
{
output << " Integer: " << derived.value
<< "\n Character: " << derived.letter
<< "\nReal number: " << derived.real;
return output;
}
从上面的示例代码中,大家应该都能看出,多重继承的语法其实是非常简单的,派生类的:
后面跟上一个逗号分隔的基类列表。
各个基类构造函数的调用顺序是多重继承指定时的顺序,这里即Base1再到Base2,而不是在派生类的构造函数中它们被使用的顺序。
测试程序,test.cpp
#include "Derived.h"
using namespace std;
int main()
{
Base1 base1(10);
Base2 base2('B');
Derived derived(7, 'A', 3.5);
cout << "Object base1 contains integer " << base1.getData()
<< "\nObject base2 contains character " << base2.getData()
<< "\nObject derived contains:\n" << derived << "\n\n";
cout << "Data members of Derived can be accessed individually:"
<< "\n Integer: " << derived.Base1::getData()
<< "\n Character: " << derived.Base2::getData()
<< "\nReal number: " << derived.getReal() << "\n\n";
cout << "Derived can be treated as an object of either base class:\n";
Base1* base1Ptr = &derived;
cout << "base1Ptr->getData() yields " << base1Ptr->getData() << "\n";
Base2* base2Ptr = &derived;
cout << "base2Ptr->getData() yields " << base2Ptr->getData() << "\n";
}
运行结果:
虽然,派生类Derived的两个基类中都有成员函数getData()
,但是从结果上来看,=调用过程并不存在二义性。
对于一个派生类对象derived当它要调用getData函数时,是存在二义性问题,所以上面的代码中使用二元作用域分辨运算符指明了它们调用的函数的作用域。
六、多重继承和virtual基类
现在我们思考一下,关于标准库basic_iostream
的定义,它是多重继承了basic_istream
和basic_ostream
两个类,且这两个类又都是从基类basic_ios
中派生而来的。在多重继承的层次结构中,这样的继承关系被称为菱形继承。
因为类basic_istream和basic_ostream都是从类basic_ios继承而来,在类basic_iostream中就可能存在一个潜在的问题——类basic_iostream可能包含了类basic_ios成员的两个副本:一个通过继承basic_istream获得,另一个通过继承basic_ostream获得。
这里我们会使用virtual基类
,来解决间接继承一个基类的多个副本的问题。
示例代码:
#include <iostream>
using namespace std;
class Base
{
public:
// 定义一个纯虚函数
virtual void print() const = 0;
};
class DerivedOne : public Base
{
public:
virtual void print() const override
{
cout << "DerivedOne\n";
}
};
class DerivedTwo : public Base
{
public:
virtual void print() const override
{
cout << "DerivedTwo\n";
}
};
class Multiple : public DerivedOne, public DerivedTwo
{
public:
virtual void print() const override
{
DerivedTwo::print();
}
};
int main()
{
Multiple both;
DerivedOne one;
DerivedTwo two;
Base* array[3];
// array[0] = &both; // 错误声明
array[1] = &one;
array[2] = &two;
for (int i = 1; i < 3; ++i)
{
array[i]->print();
}
}
运行结果:
将代码改成下面展示的这样
#include <iostream>
using namespace std;
class Base
{
public:
// 定义一个纯虚函数
virtual void print() const = 0;
};
class DerivedOne : virtual public Base
{
public:
virtual void print() const override
{
cout << "DerivedOne\n";
}
};
class DerivedTwo : virtual public Base
{
public:
virtual void print() const override
{
cout << "DerivedTwo\n";
}
};
class Multiple : public DerivedOne, public DerivedTwo
{
public:
virtual void print() const override
{
DerivedTwo::print();
}
};
int main()
{
Multiple both;
DerivedOne one;
DerivedTwo two;
Base* array[3];
array[0] = &both; // 错误声明
array[1] = &one;
array[2] = &two;
for (int i = 0; i < 3; ++i)
{
array[i]->print();
}
}
运行结果:
上面两个版本的代码中唯一的不同在于两个从抽象基类中的继承的类是使用virtual
的方式进行继承,即class DerivedOne : virtual public Base
。
这个两个类均由类Base继承而来,它们都包含一个Base子对象。virtual继承的优点直到类Multiple继承两个类时才显现出来,因为这两个类都使用了virtual继承来继承基类Base的成员,所以编译器保证只有一个Base类的子对象继承到类Multiple中,这样就消除了编译器产生的二义性错误。