c++笔记(四)

一、默认情况下,c++假定通过作用域运算符访问的名字只是名字而不是类型:

/*T是一个模板类型参数,式子解释为:类T的static成员value_type与p相乘*/
T::value_type * p; 

如果希望使用一个模板参数的类型成员,必须显式告诉编译器该名字是一个类型,使用typename关键字:

/*T是一个模板类型参数,式子解释为:类T中有个typedef的类型成员value_type,p是指向value_type的指针*/
typename T::value_type * p; 

二、当某个文件提供了模板的实例化定义,其他文件就可以仅仅使用extern声明来使用这些模板,而不必在本文件中实例化。

//templateBuild.cpp
  template class A<int>;
  template int f(const int &);
  //..

//useTemplate.cpp
  extern template class A<int>;       //无须在此文件实例化
  extern template int f(const int &); //无须在此文件实例化
  A<int> a;
  int i = 1;
  f(i);
  //..

编译此程序时,templateBuild.o 和 useTemplate.o必须链接到一起。
在类模板的实例化定义中,会实例化所有成员而不管其是否被用到,因此用来实例化模板的参数它的类型必须能用于模板的所有成员函数。

三、模板实参允许的类型转换:

  • 顶层const的增删。
  • 非引用参数数组和函数到指针的转换。
    其他类型转换:算术转换,子类向父类的转换等等都不能应用与模板实参。
template<typename T> T f1(T, T);
template<typename T> T f2(const T&, const T&);

int a[10], b[20];
long lng = 1;
//如下调用:
f1(a, b);//ok,实例化为int* f1(int*, int*)
f2(a, b);//error,数组类型不匹配,引用形参不会把数组向指针转换
//如下调用是错误的:
f1(lng, 1);//无法进行实参推导,不会进行实参转换
//如果显式指定模板实参,则是允许正常的类型转换:
f1<long>(lng, 1);//正确初始化long f1(long, long)
f1<int>(lng, 1);//正确初始化int f1(int, int)

四、可变参数函数的参数转发:

template<typename... Args>
void sender(Args&&... args)//通用引用接受任意类型参数
{
  receiver(std::forward<Args>(args)...);//forward保持参数信息
}

上面的包扩展既包括对模板类型参数Args的扩展,也包括对函数参数args的扩展。类似:receiver(std::forward<Ti>(ti));

五、模板及其特例化版本应该声明在同一个头文件里,所有同名模板的盛明应该放在前面,然后是其特例化版本。

六、前向声明:可以声明一个类而不定义它。这个声明,有时候被称为前向声明(forward declaration)。在声明之后,定义之前,该类是一个不完全类型(incompete type),即已知是一个类型,但不知道包含哪些成员。不完全类型只能以有限方式使用,不能定义该类型的对象,不完全类型只能用于定义指向该类型的指针及引用,或者用于声明(而不是定义)使用该类型作为形参类型或返回类型的函数
前向声明好处是: 不必要的#include会增加编译时间。混乱随意的#include可能导致循环#include,可能出现编译错误。

七、内联命名空间:
内联命名空间旨在通过”版本”的概念,来实现库的演化。考虑如下代码:

// 文件:V99.h
 inline namespace V99 {
         void f(int);     // 对V98版本进行改进
         void f(double);  // 新特性
         // …
 }
 // 文件:V98.h
 namespace V98 {
     void f(int);        // V98版本只实现基本功能
     // …
 }
 // 文件:Mine.h
 namespace Mine {
 #include “V99.h”
 #include “V98.h”
 }

上述命名空间Mine中同时包含了较新的版本(V99)以及早期的版本(V98),如果你需要显式使用(某个版本的函数),你可以:

#include “Mine.h”
using namespace Mine;
// …
V98::f(1);        // 早期版本
V99::f(1);        // 较新版本
f(1);            // 默认版本

此处的要点在于,被inline修饰的内联命名空间,其内部所包含的所有类/函数/变量等声明,看起来就好像是直接在外围的命名空间中进行声明的一样。(译注:我们注意到,这里的f(1)函数调用相当于显式调用Mine::V99::f(1),使用inline关键字定义的内联名字空间成为默认名字空间。 (就像内联函数一样,内联的名字空间被嵌入到它的外围名字空间,成为外围名字空间的一部分。 )
inline描述符是一个非常“静态(static)”及面向实现的设施,它由库的设计者选择在(某个版本namespace之前)放置,且一旦选定则库的所有使用者只能被动接受(即命名空间的作者可以通过放置inline描述符来表示当前最新的命名空间是哪个,所以对用户来说,这个选择是“静态”的:用户无权判断哪个命名空间是最新的)。因此,Mine命名空间的用户没法选择说:“我想要默认的命名空间为V98,而非V99”。

八、未命名的命名空间:
在匿名命名空间中有东西意味着它在本地翻译单元 (.cpp文件及其所有内容),这意味着如果另一个具有相同名称的符号在别处定义,则不会违反一个定义规则 (ODR)。

这与具有静态全局变量或静态函数的C方式相同,但它也可以用于类定义(在C ++中应该使用而不是static )。

同一文件中的所有匿名命名空间都被视为相同的命名空间,并且不同文件中的所有匿名命名空间都是不同的。 匿名命名空间相当于:

namespace __unique_compiler_generated_identifer0x42 {
    ...
}
using namespace __unique_compiler_generated_identifer0x42;

使用匿名命名空间还可以提高性能。 由于命名空间内的符号不需要任何外部链接,因此编译器可以更自由地在名称空间内对代码执行主动优化。 例如,一个循环中多次调用的函数可以内联,而不会对代码大小产生任何影响。

未命名的命名空间将类,变量,函数和对象的访问权限限制在其定义的文件中。 未命名的命名空间功能类似于C / C ++中的static关键字。 static关键字将全局变量和函数的访问权限限制在定义它们的文件中。未命名的命名空间和static关键字之间存在差异,因为其中的未命名的命名空间比静态有优势。static关键字可以与变量,函数和对象一起使用,但不能与用户定义的类一起使用。
例如:

static int x;  // Correct 

但,

static class xyz {/*Body of class*/} //Wrong
static structure {/*Body of structure*/} //Wrong

但是对于未命名的命名空间也是一样的。 例如,

 namespace {
           class xyz {/*Body of class*/}
           static structure {/*Body of structure*/}
  } //Correct

九、using指示和using声明:
using声明只是简单地令名字在局部作用域内有效:

void f()
{
  using std::cout;//简单地令std::cout在f内部有效
  //..
}

using指示是将整个命名空间的所有内容注入到使用using指示处的最近的外层作用域,往往容易产生冲突:

namespace Jack
{ int i = 1; }

int i = 2;

namespace Rose
{
  using namespace Jack;
  ++i;//二义性错误,是Jack::i还是::i?
  ++Jack::i;//OK
  ++::i;//OK
}

十、实参查找规则的一个例外:当给函数传递一个类类型的对象时,除了常规的作用域查找,还会查找实参类所属的命名空间:

std::string s;
std::cin >> s; //这里的operator>>定义在string类中,string类又在std空间中
               //这里编译器还会查找cin和string所属的std空间
               //所以我们不需要using std::operator>>

十一、友元声明中第一次出现了一个未另外声明的类或函数,那么我们会认为它是最近的外层命名空间的成员。

namespace A
{
  class C
  { friend void f();};
}//f()隐式地声明在了空间A里
//为了能被找到,我们显式地在类外声明上述友元函数:
namespace A
{
  void f();
}

十二、不要delete一个非new分配的指针,因为一般这样的指针在栈中分配,由系统管理释放,不需要delete,否则会二次释放。

十三、虚基类:虚基类是用关键字virtual声明继承的父类,即便该基类在多条链路上被一个子类继承,但是该子类中只包含一个该虚基类的备份,虚基类主要用来解决继承中的二义性问题,这就是是虚基类的作用所在。

//F是虚基类,S1,S2虚继承自F
class F {};
class S1 :virtual public F {};
class S2 :virtual public F {};

虚继承是为了:

//防止F在多条链路被SS继承时SS含有多份F的子部分。
class SS : public S1, S2 {};

虚派生只会影响从指定了虚基类的派生类中进一步派生出来的子类,而对派生类本身没有任何影响。一般将位于中间层次的基类声明为虚继承。

十四、虚继承与构造函数:

  • (1)如果在虚基类中定义了带有参数的构造函数,且没有定义默认形式的构造函数,则在整个继承过程中,所有直接或间接的派生类都必须在构造函数的成员初始化表中列出对虚基类构造函数的调用。
  • (2)建立一个对象时,如果这个对象中含有从虚基类继承来的成员,则虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的,该派生类的其他基类对虚基类的调用构造函数则被忽略。
  • (3)若在同一层次中同时包含虚基类和非虚基类,那么先调用虚基类的构造函数,再调用非虚基类的构造函数,最后调用派生类的构造函数,析构则相反。
class F { //虚基类
public:
    F(int ii = 0, string ss = "") :i(ii), s(ss) {} int i; string s;
};

class S1 :virtual public F {
public:
    S1(int ii = 0, string ss = "", float ff = 0.0) : F(ii, ss), f(ff) {}
    float f;
};
class S2 :virtual public F {
public:
    S2(int ii = 0, string ss = "", float ff = 0.0) : F(ii, ss), f1(ff) {}
    float f1;
};

class SS : public S1, S2 {
public:
    SS(int ii = 0, string ss = "", float ff = 0.0, char cc = ' ') : F(ii, ss), S1(ii, ss, ff), S2(ii, ss, ff), c(cc) {}
    char c;
};//构造SS的对象时,先调用F的构造函数,再S1,S2的构造函数,其中虚基类F的部分被忽略掉,最后构造SS独有的部分

十五、RTTI是Runtime Type Identification的缩写,意思是运行时类型识别。C++引入这个机制是为了让程序在运行时能根据基类的指针或引用来获得该指针或引用所指的对象的实际类型。
C++通过以下的两个操作提供RTTI:
(1)typeid运算符,该运算符返回其表达式或类型名的实际类型。
(2)dynamic_cast运算符,该运算符将基类的指针或引用安全地转换为派生类类型的指针或引用。

当typeid中的操作数是如下情况之一时,typeid运算符需要在程序运行时计算类型,因为其操作数的类型在编译时期是不能被确定的。
(1)一个指向含有virtual函数的类对象的指针的解引用
(2)一个指向含有virtual函数的类对象的引用

class F {
public:
    virtual void f() {}
};

class S : public F 
{
public:
    void f() override {}
};

void printTypeid(const F* const pf)
{
    cout << typeid(*pf).name() << endl;
}
int main()
{
    F f;
    S s;
    printTypeid(&f);  //class F
    printTypeid(&s);  //class S
}

dynamic_cast运算符:
把一个基类类型的指针或引用转换至继承架构的末端某一个派生类类型的指针或引用被称为向下转型(downcast)。dynamic_cast运算符的作用是安全而有效地进行向下转型。

使用RTTI:

class F 
{
  friend bool operator==(const F& lhs, const F& rhs);
  //..
protected:
  virtual bool equal(const F&) const;
  //..  
};
bool F::equal(const F& rhs) const
{
  //比较操作
};

class S : public F
{
protected:
  bool equal(const F&) const override;
};
bool S::equal(const F& rhs) const
{
  const S& srhs = dynamic_cast<const S&>(rhs);//比较子类对象时需要转换回本质上真正的类型,即子类类型
                     //这个转换不会出错
}

bool operator==(const F& lhs, const F& rhs)
{
  return typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
}

猜你喜欢

转载自blog.csdn.net/qq_38216239/article/details/80802291
今日推荐