6

C++编程思想-读书简记

 3 years ago
source link: http://lanbing510.info/2014/08/15/Thinking-In-CPlus.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.



C++编程思想反反复复读过三遍,每次都有不一样的理解和收获,下面是一些备忘记录。

第一章 对象导言


第二章 对象的创建与使用


1 如果两个加引号的字符数组相邻,并且它们之间没有标点,编译器会将其组成单个字符数组。

2 调用其他程序,只需包含头文件,调用system()函数即可。

3 文件读入除了cin,还可以ifstream in("file"); getline(in,s); 注意getline会丢弃换行符。

第三章 C++中的C


1 系统头文件和中定义了不同数据类型可能存储的最大最小值,c++中用和代替。

2 全局变量:在所有函数体的外部定义的变量。如果在一个文件中使用extern关键字来声明另一个文件中存在的全局变量,这个文件即可使用此变量。

3 局部变量:出现在一个作用域内,需要注意的是register变量,其作用是告诉编译器,“尽可能快的访问这个变量”;register只能在一个块中声明(不可能有全局的或静态的register变量);不可得到或计算register变量的地址。

4 静态变量:变量的值在程序整个声明期都存在,初始化只在函数第一次调用时执行;static的第二层意思是,在某个作用域外不可访问。

5 常量const,必须有初始值。

6 volatile变量,和const,volatile告诉编译器“不知道何时会改变”;编译器不进行优化时,volatile可能不起作用,但当开始优化时(当编译器开始寻找冗余的读入时),可以防止出现重大错误。

7 c++的显式转换

操作 说明 static_cast 用于“良性”和“适度良性”转换,包括不用强制转换(例如自动类型转换) const_cast 对“const” 和/或 “volatile”进行转换 reinterpret_cast 转换为完全不同的意思。为了安全使用它,关键必须转换回原来的类型。转换成的类型一般只能用于未操作,否则就是为了其他隐秘的目的。这是所有转换类型中最危险的 dynamic_cast 用于类型安全的向下转换

static_cast 包括编译器允许我们所做的不用强制类型转换的“安全”变换和不太安全但清楚定义的变换。包含典型的强制类型转换、窄化变换、使用void*的强制变换(C++中不用转换是不允许从void*赋值的,不像C)、隐式类型转换盒类层次的静态定位。

const_cast 不用转换是不能将const指针赋给非const指针的。

//: C03:const_cast.cpp
int main() {
  const int i = 0;
  int* j = (int*)&i; // Deprecated form
  j  = const_cast<int*>(&i); // Preferred
  // Can't do simultaneous additional casting:
//! long* l = const_cast<long*>(&i); // Error
  volatile int k = 0;
  int* u = const_cast<int*>(&k);
} ///:~

reinterpret_cast 思想是当需要使用的时候,所得到的东西已经不同,以至于他不能用于类型的原来目的,除非再次把他转换回来。

//: C03:reinterpret_cast.cpp
#include <iostream>
using namespace std;
const int sz = 100;

struct X { int a[sz]; };

void print(X* x) {
  for(int i = 0; i < sz; i++)
    cout << x->a[i] << ' ';
  cout << endl << "--------------------" << endl;
}

int main() {
  X x;
  print(&x);
  int* xp = reinterpret_cast<int*>(&x);
  for(int* i = xp; i < xp + sz; i++)
    *i = 0;
  // Can't use xp as an X* at this point
  // unless you cast it back:
  print(reinterpret_cast<X*>(xp));
  // In this example, you can also just use
  // the original identifier:
  print(&x);
} ///:~

8 创建复合类型

typedef 原类型名 别名;

c语言定义结构体变量需要加上 struct 关键字(struct Structurel s1,s2;),或者声明结构体时加上typedef。struct的名字不必和typedef的名字相同,但一般使用相同的名字。如下:

//: C03:SelfReferential.cpp
// Allowing a struct to refer to itself

typedef struct SelfReferential {
  int i;
  SelfReferential* sr; // Head spinning yet?
} SelfReferential;

int main() {
  SelfReferential sr1, sr2;
  sr1.sr = &sr2;
  sr2.sr = &sr1;
  sr1.i = 47;
  sr2.i = 1024;
} ///:~

9 使用枚举提高程序清晰度

//: C03:Enum.cpp
// Keeping track of shapes

enum ShapeType {
  circle,
  square,
  rectangle
};  // Must end with a semicolon like a struct

int main() {
  ShapeType shape = circle;
  // Activities here....
  // Now do something based on what the shape is:
  switch(shape) {
    case circle:  /* circle stuff */ break;
    case square:  /* square stuff */ break;
    case rectangle:  /* rectangle stuff */ break;
  }
} ///:~

注意c++中不能写为shape++,但c可以。

10 提供的atoi atol atof 可以将assii字符转换为int long double浮点数。

11 #define P(EX) cout<<#EX<<": "<<EX<<endl; #为字符串化操作符,讲锁连接的部分转化为一个字符串,##为字符串连接符,将两个字符串连接成一个字符串,均在预处理时完成字符串的替换。

12 中assert宏,当断言不为真的时候会出错并给出信息。 assert(i==100); 完成调试后,加上#define NDEBUG 可以消除宏产生的代码。

13 指向函数的指针数组,下面是一个表格式驱动码的例子,可以根据状态变量去选择执行函数。

//: C03:FunctionTable.cpp
// Using an array of pointers to functions
#include <iostream>
using namespace std;

// A macro to define dummy functions:
#define DF(N) void N() { \
   cout << "function " #N " called..." << endl; }

DF(a); DF(b); DF(c); DF(d); DF(e); DF(f); DF(g);

void (*func_table[])() = { a, b, c, d, e, f, g };

int main() {
  while(1) {
    cout << "press a key from 'a' to 'g' "
      "or q to quit" << endl;
    char c, cr;
    cin.get(c); cin.get(cr); // second one for CR
    if ( c == 'q' ) 
      break; // ... out of while(1)
    if ( c < 'a' || c > 'g' ) 
      continue;
    (*func_table[c - 'a'])();
  }
} ///:~

第四章 数据抽象


1 c++允许将任何类型的指针赋值给void*,但不允许将void指针赋给任何其他类型的指针。

2 ::a,全局域解析符。

第五章 隐藏实现


1 private关键字意味着,除了该类型的创建者和类的内部成员函数之外,任何人不能访问;protected和private基本相似,只有一点不同:继承的结构可以访问protected成员,但不能访问private成员。

第六章 初始化与清除


1 就像其他成员函数被调用一样。传递到构造函数的第一个(秘密)参数是this指针,也就是调用这一函数的对象的地址,不过构造函数来说,this指针指向一个没有初始化的内存块,其作用就是正确的初始化该内存块。

2 默认构造函数就是不带任何参数的构造函数;当一个结构中没有构造函数的时候,编译器会自动为他创建一个,一旦有了一个构造函数,编译器救回确保不管什么情况下他总会被调用,一旦有构造函数但没有默认构函数,V v这样的定义总会出现编译错误。

第七章 函数重载与默认参数


1 只能通过范围和参数来重载,不能通过返回值来重载。

2 union与class不同之处在于存储数据的方式,union不能在继承中作为基类。

3 默认参数的使用有两条规则,一、只有参数列表后部参数才是可默认的;二、一旦一个函数调用中开始使用默认函数,这个参数后面的所有参数必须是默认的。

4 函数声明时可以没有标识符,如void f(int x, int =0, float =1.1),c++中函数定义时也并不一定需要标识符,如void f(int x, int , float flt),中间的参数为占位符,这样的目的是以后可以修改函数定义而不必修改所有的函数调用。

第八章 常量


1 c++中的const默认为内部连接,通常c++编译器不为其创建存储空间,而是将定义保存在符号表里;但如果加上extern后,就强制进行了内存分配(另外还有一种情况,如取一个const地址)。

2 const可以用于集合,这时同样会分配内存,const意味着不能改变的一块存储空间;然后不能再编译期间使用它的值,因为编译器在编译期间不需要知道存储的内容。

3 当处理const指针时,编译器仍将努力避免存储分配并进行常量折叠

表达式 解释 const int* u 等价于 int const* u 表示u是一个指针,指向一个const int可以改变指针,但不能改变指向的内容 int* const w w 是一个指针,这个指针式指向int的const指针可以改变指向的内容,指针本身不能变 const int* const x const指针指向const对象都不可以改变

4 对于内部类型,按值返回常量是无关紧要的;但对于用户定义的类型,按值常量返回是很重要的,使得返回的对象不能使一个左值。

5 临时量会自动成为常量,f1(f2())如果f1是按值传递没有问题,如果是按引用传值但不是const引用,这种情况会导致编译错误。

6 类的const

为了保证一个类对象为常量,引进了const成员函数,const成员函数只能对于const对象调用在类里简历的普通(非staticd )const时,不能给它初值,这个初始化工作必须放在构造函数里,并且只能放在初始化列表里如果想让一个类在编译期间拥有常量成员,使用static const,在不支持static const的版本中,可以使用不带实例的无标记enum(枚举在编译器间必须有值)const成员函数,修饰符const必须放在函数参数表的后面,此类函数不能以任何方式改变非const成员及调用非const成员函数,如果想在const成员函数里改变某些数据,一种是可以使用强制转换常量性,((Y*)this)->i++或者(const_cast(this))->i++;常用的是加入关键字mutable : mutable int i\*>

第九章 内联函数


1 宏的实现是用预处理器而不是编译器,没有了参数压栈、生成汇编的call、返回参数、执行汇编的return等开销,但在c++中使用预处理宏存在两个问题:第一个在C中也存在,看起来宏像个函数调用,但并不总是这样,隐藏了难以发现的错误;第二个问题是预处理器不允许访问类的成员数据,意味着预处理器宏不能用作类的成员函数。

2 #define 的空格很脆弱 #define F (x) (x+1) F(1)--> (x) (x+1) 但如果#define F(x) (x+1) F (1)调用是没问题的;用大写字母标记宏。

3 在c++中,宏的概念是作为内联联函数来实现的,内联函数能够像普通函数一样具有我们期望的任何行为,唯一不同的是内联函数在适当的地方像宏一样展开,所以不需要函数调用的开销;声明为内联的方法是在函数前加inlien关键字,一般放在头文件。

4 任何类内部定义的函数,自动地成为内联函数。

5 当编译器遇到一个内联函数:对于任何函数,编译器在它的符号表里放入函数类型(名字,参数类型,返回类型),当编译器看到内联函数和对内联函数体进行分析没发现错误时,就将对应于函数体的代码也放入符号表,调用时带入即可。

6 太复杂的代码和需要显示或隐性取函数地址的代码都不能执行内联。

7 c++规定,只有在类声明结束后其中的内联才会被计算。

第十章 名字控制


1 静态对象存储在程序的静态数据区,如果没有为内部类型的静态变量提供一个初始值的话,编译器会确保在开始时为它初始化为零;所有全局对象都是隐含静态存储的。

2 静态对象的析构函数是在程序从main中退出时,或者标准的函数exit被调用时才被调用,多数情况下main函数的结尾也是调用exit来结束程序的;这意味着析构函数内部使用exit是很危险的,会导致无穷的递归;可以用标准的c库函数atexit来制定当程序跳出main或调用exit之前应执行的操作,atexit注册的函数可以在所有对象的析构函数之前被调用。

3 namespace只能在全局范围内使用,但他们可以互相嵌套;namespace定义的结尾,右花括号的后面不必跟一个分号;namespace可以再多个头文件中用一个标识符来定义,就好像重复定义一个类一样;namespace的名字可以用另外一个名字做他的别名(namespace a=std);可以使用未命名的名字空间,但一个翻译单元只能有一个。

4 可以使用声明(using declaration)一次性引入名字到当前范围内;这种方法不像使用指令(using directive)那样吧名字档次当前范围内的全局名来看待,using声明时再当前范围之内进行的一个声明,这意味着在这个范围内可以不顾来着using指令的名字。

//: C10:UsingDeclaration.h
#ifndef USINGDECLARATION_H
#define USINGDECLARATION_H
namespace U {
  inline void f() {}
  inline void g() {}
}
namespace V {
  inline void f() {}
  inline void g() {}
} 
#endif // USINGDECLARATION_H ///:~
//: C10:UsingDeclaration1.cpp
#include "UsingDeclaration.h"
void h() {
  using namespace U; // Using directive
  using V::f; // Using declaration
  f(); // Calls V::f();
  U::f(); // Must fully qualify to call
}
int main() {} ///:~

using声明给出了标识符完整的名字,但没有类型方面的信息,也就是说,using声明可以一次性声明相同名字重载的函数;另外,using声明可以引起一个函数用相同的类型来重载(但在使用时会有不确定性)

5 静态数据成员的定义必须在类的外部而且只能定义一次 int A::i =1;。

6 静态成员函数为类的全体对象服务而不是为类的特殊对象服务,可以用普通的方法调用静态成员函数,更典型的方法是自我调用: X::f(); ;静态成员函数不能访问一般的数据成员,只能访问静态数据成员,也只能调用其他的静态成员函数,静态成员函数没有this指针。

7 将类的一个静态数据成员放到类的内部,并把构造函数变为私有,这样就是设计模式中的单件模式,如下例中Egg类只有一个唯一的对象存在。

//: C10:Singleton.cpp
// Static member of same type, ensures that
// only one object of this type exists.
// Also referred to as the "singleton" pattern.
#include <iostream>
using namespace std;

class Egg {
  static Egg e;
  int i;
  Egg(int ii) : i(ii) {}
  Egg(const Egg&); // Prevent copy-construction
public:
  static Egg* instance() { return &e; }
  int val() const { return i; }
};

Egg Egg::e(47);

int main() {
//!  Egg x(1); // Error -- can't create an Egg
  // You can access the single instance:
  cout << Egg::instance()->val() << endl;
} ///:~

为了完全繁殖创建其他对象,还需要增加一个拷贝构造函数的私有函数,否则还可以进行Egg e=*Egg::instance() 或者 Egg e(*Egg::instance())进行创建

8 静态初始化的相依性:如果静态对象位于不同的文件中不能确定这些静态对象的初始化顺序,如果具有相依性,则会引起问题,解决的方法有:

一、不用它或者如果实在要用就把关键的静态对象的定义放在一个头文件中,保证以正确的顺序初始化

二、如果确信把静态对象放在几个不同的翻译单元中是不可避免的——如在编写一个库时,则可以用以下两个技术

A 在库的头文件中加上一个额外的类,这个类负责静态对象的动态初始化,如下面的简单例子:

//: C10:Initializer.h
// Static initialization technique
#ifndef INITIALIZER_H
#define INITIALIZER_H
#include <iostream>
extern int x; // Declarations, not definitions
extern int y;

class Initializer {
  static int initCount;
public:
  Initializer() {
    std::cout << "Initializer()" << std::endl;
    // Initialize first time only
    if(initCount++ == 0) {
      std::cout << "performing initialization"
                << std::endl;
      x = 100;
      y = 200;
    }
  }
  ~Initializer() {
    std::cout << "~Initializer()" << std::endl;
    // Clean up last time only
    if(--initCount == 0) {
      std::cout << "performing cleanup" 
                << std::endl;
      // Any necessary cleanup here
    }
  }
};
// The following creates one object in each
// file where Initializer.h is included, but that
// object is only visible within that file:
static Initializer init;
#endif // INITIALIZER_H ///:~

当第一次包含Initializer.h的翻译单元被初始化时,initCount为零,这时初始化已完成,对其余的翻译单元,initCount不会为零,并忽略其初始化。

B 第二个技术基于这样的事实:函数内部的静态对象,在函数第一次被调用时初始化,且只初始化一次,由此我们可以将静态对象用函数包装:

//: C10:Technique2.cpp
#include "Dependency2.h"
using namespace std;

// Simulate the dependency problem:
extern Dependency1 dep1;
Dependency2 dep2(dep1);
Dependency1 dep1;

// But if it happens in this order it works OK:
Dependency1 dep1b;
Dependency2 dep2b(dep1b);

// Wrapping static objects in functions succeeds
Dependency1& d1() {
  static Dependency1 dep1;
  return dep1;
}

Dependency2& d2() {
  static Dependency2 dep2(d1());
  return dep2;
}

int main() {
  Dependency2& dep2 = d2();
} ///:~

这样dep1,dep2 就不会出现初始化顺序的问题

9 如果在c++中编写一个程序需要c的库,可以用替代连接说明来实现: extern “C” { #include "xxx.h"} 、extern "C" float f(int a)、extern “C” {float f(); float g(int a ,int b);}

第十一章 引用和拷贝构造函数


1 当引用被创建时,必须初始化,且一旦一个引用被初始化为指向一个对象,它就不能改变为另一个对象的引用,不可以有NULL引用。

2 拷贝构造函数,它常被称为X(X&)(X引用的X),在函数调用时,这个构造函数式控制通过传值方式传递和返回用户定义类型的根本所在。

3 如果设计了拷贝构造函数,当从现有的对象创建新对象时,编译器将不使用位拷贝,编译器总是调用我们的拷贝构造函数。

4 如果我们加入了拷贝构造函数,我们就告诉了编译器我们将自己处理构造函数的创建,编译器将不再创建默认的构造函数,并且除非我们显式的创建一个默认的构造函数,如同下例中为WithCC所做的那样,否则将会出错。

DefaultCopyConstructor.cpp

为了对使用组合(和继承的方法)的类创建拷贝构造函数,编译器递归地为所有的成员对象和基类调用拷贝构造函数,如果成员对象还有别的对象,那么后者的拷贝构造函数也将被调用。

5 默认的是拷贝构造函数是按位拷贝;仅当准备用按值传递的方式传递类对象时,才需要拷贝构造函数。

6 声明一个私有的拷贝构造函数可以防止按值传递方式传递。

7 下面的例子说明了如何使用指向数据成员的指针,成员指针的语法要求选择一个对象的同时间接引用成员指针:选择一个类的成员意味着类中偏移,需要把这个偏移和具体对象的开始地址结合。

//: C11:PointerToMemberData.cpp
#include <iostream>
using namespace std;

class Data {
public:  
  int a, b, c; 
  void print() const {
    cout << "a = " << a << ", b = " << b
         << ", c = " << c << endl;
  }
};

int main() {
  Data d, *dp = &d;
  int Data::*pmInt = &Data::a;
  dp->*pmInt = 47;
  pmInt = &Data::b;
  d.*pmInt = 48;
  pmInt = &Data::c;
  dp->*pmInt = 49;
  dp->print();
} ///:~

8 通过给普通函数插入类名和作用域运算符就可以定义一个指向成员函数的指针,其可以在创建时初始化,也可以在任何其他时候;不像非成员函数,当获取成员函数地址时,&符号是必须的。

//: C11:PmemFunDefinition.cpp
class Simple2 { 
public: 
  int f(float) const { return 1; }
};
int (Simple2::*fp)(float) const;
int (Simple2::*fp2)(float) const = &Simple2::f;
int main() {
  fp = &Simple2::f;
} ///:~

9 程序运行时,通过改变指向所指向的内容可以选择可改变我们的行为,成员指针也如此,允许运行时选择一个成员,注意下面例子所用语法。

//: C11:PointerToMemberFunction2.cpp
#include <iostream>
using namespace std;

class Widget {
  void f(int) const { cout << "Widget::f()\n"; }
  void g(int) const { cout << "Widget::g()\n"; }
  void h(int) const { cout << "Widget::h()\n"; }
  void i(int) const { cout << "Widget::i()\n"; }
  enum { cnt = 4 };
  void (Widget::*fptr[cnt])(int) const;
public:
  Widget() {
    fptr[0] = &Widget::f; // Full spec required
    fptr[1] = &Widget::g; 
    fptr[2] = &Widget::h;
    fptr[3] = &Widget::i;
  }
  void select(int i, int j) {
    if(i < 0 || i >= cnt) return;
    (this->*fptr[i])(j); // 当被间接引用时,语法也需要 成员指针总是和一个对象绑定在一起
  }
  int count() { return cnt; }
};

int main() {
  Widget w;
  for(int i = 0; i < w.count(); i++)
    w.select(i, 47);
} ///:~

第十二章 运算符重载


1 运算符的定义类似一个普通函数的定义,只是函数的名字由关键字operator及其后紧跟的运算符组成。 2 函数参数表中参数的个数取决于: 一、运算符是一元的还是二元的;二、运算符被定义为全局的还是成员的两个因素。

3 一元运算符重载的实例如下,注意Integer类使用的是全局运算符重载,byte使用的是成员运算符重载。

OverloadingUnaryOperators.cpp

4 二元运算符的重载。

Integer.h Integer.cpp IntegerTest.cpp

Byte.h ByteTest.cpp

5 参数和返回值

一、对于任何函数参数,如果仅需要从参数中读而不改变它,默认地应当作为const引用来传递,当函数是一个类成员函数时就转换为const成员函数;只有会改变左侧参数的运算符赋值如+=,和operator=,左侧参数不是常量,按引用传之。

二、返回值的类型取决于运算符的具体含义,如果结果是产生一个新值,就需要产生一个作为返回值的新对象,例如Integer::operator+ 这个对象作为一个常量通过传值方式返回,作为一个左值结果不会改变。

三、所有赋值运算符的返回值对于左值应该是非常量引用。

四、通过传值方式返回要创建的新对象时,要使用临时对象语法,例如operator+,return Integer(left.i, right.i); 这样编译器会直接把这个对象创建在外部返回值的内存单元,而不是创建一个局部对象。

6 不常用的运算符

下标运算符operator[],必须是成员函数并且值接受一个参数;运算符new和delete。

7 当希望一个对象表现的像一个指针,通常要用到operator->,如果想用类包装一个指针以使指针安全,或是在迭代器普通的用法中,这样做会特别有用(迭代器是一个对象,这个对象可以作用于其他对象的容器或集合上,每次选择他们中的一个,而不用提供对容器的直接访问)。

8 指针间接引用运算符(operator->)一定是一个成员函数它有着额外的、非典型的限制:他必须返回一个对象(或对象的引用),该对象也有一个指针间接引用运算符;或者必须返回一个指针,被用于选择指针间接引用运算符箭头所指向的内容,下面是个简单的例子。

SmartPointer.cpp

例子中尽管sp实际上没有成员函数f()和g(),但指针间接引用运算符自动地位用SmartPointer::operator-> 返回的Obj*调用那些函数。

更常见的是,“灵巧指针”和“迭代器”类嵌入他所服务的类中,前面的例子可以按以下方式重写,在OjbContainer中嵌入SmartPointer。

NestedSmartPointer.cpp

9 operator->* 是一个二元运算符,它是专门为模仿前一章介绍的内部数据类型的成员指针行为而提供的(对于一个普通函数指针,调用的时候 (fp)(1),这个运算符想要使成员函数指针的调用为(w->fp)(1) ,这就要求operator->*必须返回一个对象,对于这个对象,可以用正在调用的成员函数为参数调用operator(),请注意operator()的调用必须是成员函数,他是唯一的允许在它里面有任意个参数的函数 ),请看下面的例子。

PointerToMemberOperator.cpp

其中23行的PMF是一个typedef,用于简化定义一个指向Dog成员函数的指向成员的指针,operator->*创建并返回一个FunctionObject对象,注意operator->*既知道指向成员的指针所调用的对象(this),又知道这个指向成员的指针,并把他们传递给存储这些值的FunctionObject构造函数。

10 不能重载的运算符:operator.、operator.* 、没有的运算符(例如求幂operator**)、用户自定义的;不能改变优先级规则。

11 运算符可能是成员运算符或非成员运算符,似乎没多大差异,应该选择哪种?总的来说,如果没有什么差异,他们应该是成员运算符,这样强调了运算符和类的联合,当左侧操作数是当前类的对象时,运算符会工作的很好;但有时左侧运算符是别的对象,这种情况通常出现在输入输出流重载 operator<< 和>>,看下面的例子。

IostreamOperatorOverloading.cpp

47行iostream的一种新类型被使用:stringstream(在中声明),该类包含一个string,并且把它转化为一个iostream。

12 所有的一元运算符和+= -= /= *= ^= &= |= %= >= <<=建议使用成员运算符;= () [] -> ->*必须使用成员;所有其他二元运算符建议使用非成员运算符重载。

13 重载赋值符operator=,注意检查一下自赋值,如下:

DogHouse(Dog* dog, const string& house)
  : p(dog), houseName(house) {}
DogHouse(const DogHouse& dh)
  : p(new Dog(dh.p, " copy-constructed")),
    houseName(dh.houseName 
      + " copy-constructed") {}
DogHouse& operator=(const DogHouse& dh) {
  // Check for self-assignment:
  if(&dh != this) {
    p = new Dog(dh.p, " assigned");
    houseName = dh.houseName + " assigned";
  }
  return *this;
}

例子中拷贝构造函数和operator=对指针所指向的内容做了一个新的拷贝,并有细狗函数删除,但是如果对象需要大量内存或过高的初始化,我们可以用引用计数来解决这个问题,见下例:

ReferenceCounting.cpp

函数attach增加一个Dog引用记录,detach减少一个,如果没有对象引用(0),则delete this;在进行修改前,必须保证所修改的Dog没有被别的使用,可以调用DogHouse::unalias(),它又进而调用Dog::unalias()来做到这14点(如果引用为1,则返回Dog指针,如果大于1,则就要赋值这个Dog) operator=处理等号左侧已创建的对象,所以它首先必须通过Dog调用detach来整理这个存储单元;如果没有其他对象使用它,这个老的Dog将被销毁,然后operator=重复拷贝构造函数的行为。

14 自动类型转换

一、第一种是使用构造函数转换(目的类执行转换),可以使用explicit关键字来组织构造函数转换,如下

//: C12:ExplicitKeyword.cpp
// Using the "explicit" keyword
class One {
public:
  One() {}
};

class Two {
public:
  explicit Two(const One&) {}
};

void f(Two) {}

int main() {
  One one;
//!  f(one); // No auto conversion allowed
  f(Two(one)); // OK -- user performs conversion
} ///:~

去掉explicit会进行自动转换

二、通过运算符重载(源类执行转换),可以创建一个成员函数,这个函数通过在关键字operator后跟随想要转换到的类型的方法,将当前类型转换为希望的。

OperatorOverloadingConversion.cpp

使用构造函数技术没办法实现从用户定义类型向内置类型的转换,这只有运算符重载可能做到。

15 当提供不止一种类型的自动转换时,会出现扇出问题(fan-out),如下:

//: C12:TypeConversionFanout.cpp
class Orange {};
class Pear {};

class Apple {
public:
  operator Orange() const;
  operator Pear() const;
};

// Overloaded eat():
void eat(Orange);
void eat(Pear);

int main() {
  Apple c;
//! eat(c);
  // Error: Apple -> Orange or Apple -> Pear ???
} ///:~

第十三章 动态对象创建


1 重载new和delete,使用new和delete的内存分配系统是为通用目的而设计的,在特殊情况下它并不能满足要求,最常见的改变分配系统的原因是出于效率考虑:也许要创建和销毁一个特点的类的非常多的对象以至于这个运算变成了速度的瓶颈,c++允许重载new和delete来实现自己的存储分配方案,重载时可以改变的只是内存分配部分。

第一个例子是重载全局new和delete:

GlobalOperatorNew.cpp

new返回的是使void*,所做的是分配内存,完成一个对象建立是在后面调用了构造函数之后;delete的参数是一个指向有new分配的内存的void*,它是在调用析构函数之后得到的指针

第二个例子是对一个类重载new和delete。

Framis.cpp

第三个是为数组重载new和delete,数组版的和单个对象版本的是一样的。

ArrayOpeartorNew.cpp

2 定位new和delete

operator new()还有其他两个不常见的用途:一、在内存的特定位置放置一个对象;二、想在调用new时能够选择不同的内存分配方案;这两个特性可以用相同的机制实现:重载的operator new()可以带一个或多个参数。

下面的例子显示了如何在衣蛾特定的存储单元里放置一个对象。

//: C13:PlacementOperatorNew.cpp
// Placement with operator new
#include <cstddef> // Size_t
#include <iostream>
using namespace std;

class X {
  int i;
public:
  X(int ii = 0) : i(ii) {
    cout << "this = " << this << endl;
  }
  ~X() {
    cout << "X::~X(): " << this << endl;
  }
  void* operator new(size_t, void* loc) {
    return loc;
  }
};

int main() {
  int l[10];
  cout << "l = " << l << endl;
  X* xp = new(l) X(47); // X at location l
  xp->X::~X(); // Explicit destructor call
  // ONLY use with placement!
} ///:~

注意第28行,new后是参数表(没有size_t参数,它由编译器处理),参数表后面是正在创建的对象的类名字;注意29行显示的调用析构函数,这种情况只有在支持operator new()的定位语法(如果用到栈上创建的对象,析构函数会调用两次出现严重错误,如果为展商创建的对象用这种方法,析构函数将被执行但内存不释放)

第十四章 继承和组合


1 构造函数的调用顺序是首先会调用基类构造函数,然后调用成员对象构造函数。

2 任何时候重新定义基类中的一个重载函数(重定义),在新类中所有其他版本则被自动的隐藏;如果基类的成员函数是虚函数,成为重写。

3 构造函数和析构函数不能被继承,必须为每一个特定的派生类分别创建,另外operator=也不能被继承;在继承过程中,如果不亲自创建这些函数,编译器会生成他们。

4 一旦决定写自己的拷贝构造函数和赋值运算符,编译器就会假定我们已知道所做的一切,并且不在像在生成的函数那样自动地调用基类版本;而如果想调用基类版本,那我们就必须亲自显式的调用他们。

5 组合(has a , 继承 is a)通常是在希望新类内部具有已存在类的功能时使用,而不是希望已存在类作为它的接口。

6 通过在基类表中去掉public或者通过显式的声明private,可以私有的继承基类;当私有继承是,我们是“照此实现”,也就是说创建的新类具有基类的所有数据和功能,但这些功能时隐藏的,所有他只是部分的内部实现;当私有继承时,基类的所有public成员都变成了private,如果希望他们当中任何一个是可视飞,只需用派生类的public部分声明他们的名字即可:

//: C14:PrivateInheritance.cpp
class Pet {
public:
  char eat() const { return 'a'; }
  int speak() const { return 2; }
  float sleep() const { return 3.0; }
  float sleep(int) const { return 4.0; }
};

class Goldfish : Pet { // Private inheritance
public:
  Pet::eat; // Name publicizes member
  Pet::sleep; // Both overloaded members exposed
};

int main() {
  Goldfish bob;
  bob.eat();
  bob.sleep();
  bob.sleep(1);
//! bob.speak();// Error: private member function
} ///:~

private继承使用的情况比较少(可能想产生像基类接口一样的接口部分,而不允许该对象的处理像一个基类对象),通常希望使用组合而不是private继承。

7 有事希望某些东西隐藏起来,但仍允许其派送类的成员访问,关键字protected就用上了,其意思是:就这个类的用户而言,它是private的,但它可被从这个类继承来的任何类使用;保护继承的派生类对其他类来说是“照此实现”,但它对于派送类和友元是is a,不常用。

8 多重继承:在继承时,只需在基类列表中增加多个类,用逗号隔开。

第十五章 多态性和虚函数


1 面向对象程序设计语言中的三个基本特征是:数据抽象、继承、多态,多态性提供了接口和具体实现之间的另一层隔离,从而将what和how分离开来。

2 虚函数仅仅在声明时需要使用关键字virtual,定义时并不需要;如果一个函数在基类中被声明为virtual,那么所有派生类它都是virtual的,在派生类中virtual函数的重定义常称为重写。

3 典型的编译器对每个包含虚函数的类创建一个表(称为VTABLE),在VTABLE中,编译器放置特定类的虚函数的地址;在每个带有虚函数的类中,编译器秘密的放置一个指针(vpointe人,VPTR),指向这个对象的VTABLE。当通过基类指针做虚函数调用时,编译器静态的插入能取得这个VPTR并在VTABLE表中查找函数地址的代码,这样就可以调用正确的函数并引起晚捆绑的发生。

4 在设计时,如果希望基类仅仅作为其派生类的一个接口,即仅想对基类进行向上的类型转换,使用它的接口但不希望用户实际的创建一个基类的对象,我们可以在类中加入至少一个纯虚函数,来使基类成为抽象类。纯虚函数使用关键字virtual,并且在其后面加上=0(virtual void f()=0;)。

5 当继承一个抽象类时,必须实现所有的纯虚函数,否则继承出的类也是一个抽象类。

6 如果对一个对象进行向上类型转换,而不使用地址或引用,就会出现对象切片。

7 我们不能在重新定义过程中修改虚函数的返回类型,但也有特例。

VariantReturn.cpp

在Cat中,eats的返回类型是指向CatFood的指针,而CatFood是派生于PetFood,返回类型是从基类函数的返回类型继承而来的,合约仍被遵守。

8 构造函数不能为虚函数,但析构函数能够且常常必须是虚函数(如下例,delete bp只调用基类的析构函数,而delete b2p基类和派生类的都会执行)。

//: C15:VirtualDestructors.cpp
// Behavior of virtual vs. non-virtual destructor
#include <iostream>
using namespace std;

class Base1 {
public:
  ~Base1() { cout << "~Base1()\n"; }
};

class Derived1 : public Base1 {
public:
  ~Derived1() { cout << "~Derived1()\n"; }
};

class Base2 {
public:
  virtual ~Base2() { cout << "~Base2()\n"; }
};

class Derived2 : public Base2 {
public:
  ~Derived2() { cout << "~Derived2()\n"; }
};

int main() {
  Base1* bp = new Derived1; // Upcast
  delete bp;
  Base2* b2p = new Derived2; // Upcast
  delete b2p;
} ///:~

9 纯虚析构函数在c++中是合法的,但在使用时有一个额外的限制:必须为纯虚函数提供一个函数体。

//: C15:PureVirtualDestructors.cpp
// Pure virtual destructors
// require a function body
#include <iostream>
using namespace std;

class Pet {
public:
  virtual ~Pet() = 0;
};

Pet::~Pet() {
  cout << "~Pet()" << endl;
}

class Dog : public Pet {
public:
  ~Dog() {
    cout << "~Dog()" << endl;
  }
};

int main() {
  Pet* p = new Dog; // Upcast
  delete p; // Virtual destructor call
} ///:~

10 在析构函数中,只有成员函数的“本地版本”被调用。

11 typeid关键字是用来检测指针类型的。

第十六章 模板介绍


1 模板参数不局限于类定义的类型,可以使用内置类型;这些参数值在编译期间变成模板的特定示例的常量,我们甚至可以对这些参数使用默认值。

Array3.h

此例使用了懒惰初始化:Array是被检查的对象数组,并且防止下标越界,类Holder很想Array,只是它有一个指向Array的指针,而不是指向类型Array的嵌入对象,该指针在构造函数中不进行初始化,而是推迟到了第一次访问。

2 两个例子

一、带有迭代器的Stack和Ptash

TStack2.h TStack2Test.cpp

TPStash2.h TPStash2Test.cpp

二、Shape

Shape.h Drawing.cpp


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK