2013年8月18日夜02:59
第一章
1、OOP的基本思想:创建抽象的数据类型。
2、类描述了特性(数据元素)和行为(功能)的对象,类实际上是数据类型。
3、向对象发送消息,调用相应的接口函数。
4、访问控制的理由:1-防止客户程序员插手他们不应当接触的部分。2-允许库设计者去改变这个类的内部工作方式,而不必担心这样做会影响客户程序员。public:对所有人都可用。private除了该类型的创建者和该类型的内部成员函数职务,任何人都不可以访问。protected和private相似,只有一点不同,即继承的类可以访问protected成员,但不能访问private成员。
5、OOP的最大优点之一:代码重用。
6、virtual关键字实现晚捆绑。virtual虚函数可用来表示出在相同家族中的类具有不同的行为。这些不同是产生多态行为的原因。把处理派生类型就如同处理其基类型的过程叫向上类型转换
如果一个成员函数是virtual的,当我们给对象发送消息时,这个对象将做正确的事情。
7、OOP的论域:抽象的数据类型、继承、多态。c++采取的方法把效率控制作为最重要的问题。
8、对象的销毁和创建:使用关键字new、delete在堆上动态创建对象
第二章
1、声明和定义:
声明是向编译器介绍名字,标识符,不占存储空间。
定义,建立变量,为名字分配空间。变量和函数可以在不同地方声明,但是只有一个定义。
定义也可以是声明,如果定义int x;之前,编译器没有发现x,编译器则把这一标识符看成是声明,并立即为它分配存储空间。
2、函数声明:int func(int,int);
空参数表:C:表示一个可带任意参数任意类型的函数,这就妨碍了类型检查。在C++中表示不带参数的函数。
变量声明:extern int a;声明一个变量但不定义它。extern也可用于函数的声明。
3、头文件:为了声明在库中已有的函数和变量,用户只需要包含头文件即可。使用#include预处理命令。
#include <header>用尖括号来指定文件时,预处理器是以特定的方式来寻找文件,一般是环境中或编译器命令行指定的某种寻找路径。
#include "local.h" 用双引号,预处理器以“定义实现的路径”来寻找文件。它通常是从当前目录开始寻找,如果文件没有找到,那么include命令就按与尖括号同样的发那个是重新开始寻找。
4、连接器如何查找库:如果连接器在目标模块列表中不能找到函数或变量的定义,它将去查找库,库有多种索引方式,连接器不必到库里查找所有目标模块,而只需浏览索引,当连接器在库中找到定义之后,就将整个目标模块而不仅仅是函数定义链接到可执行程序。注意:仅仅是库中波阿含所需定义的目标模块加入链接,而不是整个库参加链接。
5、#include <cstdlib> system("hello");
第三章
1、数据类型可以是内部的或抽象的,作为一个类,一般被称为抽象数据类型。
系统头文件<limits>中定义了不同的数据类型可能存储的最大值和最小值:、const ulong ULMAX=numeric_limits<ulong>::max();<climits>
2、浮点数的大小等级:float、double、long double。没有long float、也没有short浮点数。
3、char默认是signed. 通过规定signed char,可以强制使用符号位。
4、 int i= int(0);
cout<<i<<endl;
cout<<&i<<endl;
cout<<static_cast<void *>(&i)<<endl;
//output:
0
0012FF7C
0012FF7C
5、通过函数传递指针允许修改外部对象。以引用传递允许一个函数去修改外部对象。指针和引用都有此功能。
6、如果指针声明为void*,它意味着任何类型的地址都可以简介引用那个指针。但是不能使用void引用。
void *vp;
int i;
vp=&i;
一旦间接引用一个void*,就会丢失关于类型的信息。这意味着在使用前必须转换为正确的类型。
int i;
void *vp=&i;
//! *vp=3;//run error,使用前需要转换。
*((int *)vp)=3;
7、作用域规则告诉我们变量的范围:在哪里创建,在哪里销毁。变量的作用域从它的定义点开始,到河定义变量之前最邻近的开括号配对的都一个闭括号。
8、寄存器变量是一个局部变量。不可能得到或计算register变量的地址。register变量只能在一个块中声明,不可能有全局的或静态的register变量。
9、静态变量。初始化只在函数第一次调用时执行。static变量的优点:在函数范围之外它是不可用的。所以他不可能被轻易的改变。static具有文件作用域。在文件的外部不可访问。尽管在文件中声明变量为extern,但是连接器不会找到它。
10、外部变量:extern. extern int i;表示i作为全局变量存在于某处。
11、常量:在C中,如果建立一个常量,必须使用预处理器:#define PI 3.14
c++引入了命名常量的概念,命名常量就像变量。只是它的值不能改变。修饰符const告诉编译器这个名字表示常量。const int x = 10;//=#define x 10
C++中,一个const必须有初始值,dec/oct:0/hex:0x
12、volatile:告诉编译器不知道何时会改变。防止编译器依据变量的稳定性做任何优化。
13、取出一个字节,一位一位输出:
for(int i=7;i>=0;i--)
{
if(val& (1<<7))
{
std::cout<<"1";
}
else
{
std::cout<<"0";
}
14、逗号运算符。int x=a++,b++,v++,c++;//x=c++;在表达式中,只产生最后一个表达式的值。
15、类型转换:
static_cast:用于良性和适度良性转换。包括不用强制转换。dynamic_cast:用于类型安全的向下转换。const_cast常量转换,从const转换到非const,从volatile转换到非volatile。
int i;
long l;
l=static_cast<long>(i);
如果取得了const对象的地址,就可以生成一个指向const的指针,不用转换是不能将它赋值给非const指针的。
const int i=0;
int *j=(int *)&i;//禁止使用
j=const_cast<int *>(&i);
long *l=const_cast<long *>(&i);//error
16、sizeof()是一个运算符,不是函数。typedef命名别名。
typedef 原类型名 别名
struct Structure1{
};
main()
{
struct Structure1 s1;//必须说struct Structure1
...
}Structure2;
main()
{
Structure2 s1;
...
18、argc>=1,argv[0]程序本身的路径和名字。
<cstdlib> 中定义atoi/atof/atol 。int x=atoi(argv[1])
19、把变量和表达式转换成字符串。#
#define PR(x) cout<<#x<<"="<<x<<"\n";
20、使用assert完成调试后,在#include <cassert>之前插入语句 #define NDEBUG ,则assert()宏失效。
21、函数指针:使用前必须给他赋一个函数的地址,就像一个数组array[10]的地址是由不带方括号的这个数组的名字array一样。函数func()的地址也是由没有参数列表的函数名func产生的,也可以使用更加明显的语法&func().
void func() {
cout << "func() called..." << endl;
}
int main() {
void (*fp)(); // Define a function pointer
fp = func; // Initialize it
(*fp)(); // Dereferencing calls the function
void (*fp2)() = func; // Define and initialize
(*fp2)();
} ///:~
fp = func;使fp获得函数func的地址
第四章
1、动态存储分配,new、delete。new表达式返回指向所请求的转却类型对象的指针。若果声称new type,返回指向type的指针,int *a=new int;delete关键字是new对应的关键字。忘记delete会造成内存泄露。
2、ifstream in("....txt");in.open(",...txt");
3、struct内部可以有成员函数,用作用域解析运算符::.
void Stash::initialize(int size){
}
4、在C中可以赋void*给任何指针,编译器可以通过。
int i=19;
void *vp=&i; //ok in both c and c++
int *ip=vp; //only acceptable in c
但是在c++中,语句不允许。c++运行将任何类型的指针赋给void*可以,但是不允许将void*指针赋给任何其他类型的指针。
5、把函数放进结构中是从C到C++中的根本改变。这使得结构体既可以描写属性,又可以描写行为。这就形成了对象的概念。
在C++中,对象是一块空间,存放着数据而且还隐含着对这些数据进行处理的操作。把函数捆绑在数据结构内部的语言是基于对象的,而不是面向对象的。
把数据和函数捆绑在一起的能力叫做封装。它有属性和行为。object.member(),是对一个对象调用一个成员函数,在面向对象的用那个噶中,称之为向一个对象发送消息。
6、面向对象可以总结为一句话:向对象发送消息。在结构的内部放入函数,结构的种新类型成为抽象数据类型,用这种结构创建的变量称为这个类型的对象或实例。调用对象的成员函数成为向这个对象发送消息,在面向对象的程序设计中断主要动作就是向对象发送消息。
7、一个struct大小是它的所有成员大小的和。当一个struct被编译器处理时,会增大额外的字节以使得边界整齐。使用sizeof(A)...
struct A {
int i[100];
};//400 bytes
struct B {
void f();
};//1 bytes
typedef struct CStashTag {
int size; // Size of each space
int quantity; // Number of storage spaces
int next; // Next empty space
// Dynamically allocated array of bytes:
unsigned char* storage;
} CStash;//16 bytes
struct Stash {
int size; // Size of each space
int quantity; // Number of storage spaces
int next; // Next empty space
// Dynamically allocated array of bytes:
unsigned char* storage;
// Functions!
void initialize(int size);
void cleanup();
int add(const void* element);
void* fetch(int index);
int count();
void inflate(int increase);
}; //16 bytes
8、接口与实现相分离,即声明与成员函数的定义分离。声明放在头文件中。
9、预处理器#define可以用来创建编译时标记。两种选择:1-#define FLAG 告诉编译器这个标记已被定义,但不指定特定的值。2-给它一个值。#define PI 3.14
无论哪种情况,预处理器都能测试该标记,检查它是否已经被定义。#ifdef FLAG 这将得到一个真值,因为上面一行已经定义。
#define 的反义:#undef:1-使得使用相同变量的#ifdef语句得到假值。2-#undef还引起预处理器停止使用宏。
#ifdef的反义#ifndef,标记还没有定义,它得到一个假值。
使用#ifndef可以避免头文件被多次包含。如果头文件被第一次包含,则这个头文件中的内容将被包含到预处理器中。
#ifndef HEARDER_H
#define HEARDER_H
...
#endif
10、不要在头文件中使用using namespace std;如果在头文件中,意味着名字空间将在包含这个头文件的任何文件中消失。
11、全局作用域解析:局部作用域为a,全局作用域为a,要在局部作用域使用全局标识符,使用作用域解析运算符。
int a;
void f() {}
struct S {
int a;
void f();
};
void S::f() {
::f(); // Would be recursive otherwise!
::a++; // Select the global a
a--; // The a at struct scope
}
int main() { S s; f(); } ///:~
S::f() 中没有作用域接卸运算符,编译器默认会选择成员函数的f()和a.
第五章
1、struct访问控制:struct可以有public、protected、private。struct默认是public。
struct A{
public:
...
private:
...
};
继承的结构可以访问protected成员。但不能访问private成员,
5、如果想允许不属于当前结构的一个成员函数访问当前结构中的数据,使用friend友元。friend可以访问private和protected成员。
可以把全局函数声明为friend,也可以把另一个结构中的成员函数甚至整个结构都声明为friend。
struct X;
struct Y {
void f(X*);
};
struct X { // Definition
private:
int i;
public:
void initialize();
friend void g(X*, int); // Global friend 声明的同时,友元
friend void Y::f(X*); // Struct member friend 成员函数在声明为友元之前声明。但要声明Y::f(X*),又必须先声明struct X .因为X*表示引用了对象X的地址,编译器不需要知道对象类型的大小,只需要知道对象的地址。所以struct X;不完全的类型声明即可。否则要必须知道X的全部定义。
friend struct Z; // Entire struct is a friend 不完全的类型说明。
friend void h();
};
void X::initialize() {
i = 0;
}
void g(X* x, int i) {
x->i = i;
}
void Y::f(X* x) {
x->i = 47;
}
struct Z {
private:
int j;
public:
void initialize();
void g(X* x);
};
void Z::initialize() {
j = 99;
}
void Z::g(X* x) {
x->i += j;
}
void h() {
X x;
x.i = 100; // Direct data manipulation
}
6、嵌套友元:嵌套结构不能自动获得访问private成员的权限。需要:1-先声明;2-friend;3-定义这个结构。
//memset(a, 0, sz * sizeof(int));
struct Holder {
private:
int a[sz];
public:
void initialize();
struct Pointer;
friend Pointer;
struct Pointer {
private:
Holder* h;
int* p;
public:
void initialize(Holder* h);
// Move around in the array:
};
};
void Holder::initialize() {
memset(a, 0, sz * sizeof(int));
}
void Holder::Pointer::initialize(Holder* rv) {
h = rv;
p = rv->a;
}
int main() {
Holder h;
Holder::Pointer hp;
h.initialize();
hp.initialize(&h);
} ///:~
7、访问控制通常是指时下细节的隐藏。原因:1-不必担心客户程序员会把内部的数据机制当做他们可使用的接口的一部分来访问。2-将具体实现和接口分离开来。客户程序员只能对public接口发送消息,这样可以改变所有声明为private的成员而不去修改客户程序员的代码。
面向对象编程:同时采用封装和访问控制。
类和struct的每个方面都是一样的,除了class中的成员默认为private,而struct中的成员默认为public。
private中的成员函数属于内部实现的部分,不属于接口部分。
8、将类包含在一个头文件中,修改类时,只需要重编译这个头文件即可,减少项目的重复编译。
第六章
1、c++中的初始化很重要,交给构造函数去完成。编译器在创建对象时自动调用构造函数。class X{public:X(){}};X a;为对象分配内存,构造函数自动被调用。传递到构造函数的第一个参数是this指针,也就是调用这一函数的对象的地址。对于构造函数来说,this指针指向一个米有被初始化的内存块,构造函数的作用就是正确的初始化该内存块。在c++中,对象的定义和初始化是集为一体的,不能只取其中之一。构造函数和析构函数都没有返回值。
2、析构函数不能带任何参数。当对象超出它的作用域时,析构函数由编译器自动调用。
3、集合的初始化:
int b[6]={0};数组大小 SZ = sizeof b / sizeof *b ;
结构也可以这样初始化
typedef struct X{
int x;
float f;
char c;
}X;
X x1={1,2.2,'c'};
构造函数通过被调用来完成初始化。
struct Y{
float f;
int i;
Y(int a):i(a){}
};
Y y1[3]={Y(1),Y(2)};
3、默认构造函数不带任何参数。当struct或class中没有构造函数时,编译器为它自动创建一个,一旦有构造函数而没有默认构造函数,当需要默认构造函数的时候,需要显式地编写默认的构造函数。
第七章
1、重载的思想:同名的函数,但是这些函数的参数列表应该不一样。同名的局部函数(类内部)和全局函数不会发生冲突。
函数重载通过范围和参数来重载。但是不能通过返回值来重载。
因为C中,总是可以调用一个函数但忽略它的返回值。
2、重载的一个重要应用:构造函数。
3、struct、class唯一的不同之处就是struct默认为public,而classs默认为private。struct也可以由够战术和析构函数。另外union也可以由构造函数、析构函数、成员函数甚至访问控制。
union U {
private: // Access control too!
int i;
float f;
public:
U(int a);
U(float b);
~U();
int read_int();
float read_float();
};
U::U(int a) { i = a; }
U::U(float b) { f = b;}
U::~U() { cout << "U::~U()\n"; }
int U::read_int() { return i; }
float U::read_float() { return f; }
union与class的唯一不同之处在于存储数据的方式。union中int和float类型的数据在同一内存中覆盖存放。但是union不能在继承时作为基类使用。
class SuperVar {
enum {
character,
integer,
floating_point
} vartype; // Define one 定义一个enum实例
union { // Anonymous union 匿名联合
char c;
int i;
float f;
};
public:
SuperVar(char ch);
SuperVar(int ii);
SuperVar(float ff);
void print();
};
SuperVar::SuperVar(char ch) {
vartype = character;
c = ch;
}
SuperVar::SuperVar(int ii) {
vartype = integer;
i = ii;
}
SuperVar::SuperVar(float ff) {
vartype = floating_point;
f = ff;
}
void SuperVar::print() {
switch (vartype) {
case character:
cout << "character: " << c << endl;
break;
case integer:
cout << "integer: " << i << endl;
break;
case floating_point:
cout << "float: " << f << endl;
break;
}
}
union没有类型名和标识符,叫做匿名联合,为这个union创建空间但不需要用标识符的方式和以点操作符'.'方式访问这个union的元素。
int main() {
union {
int i;
float f;
};
// Access members without using qualifiers:
i = 12;
f = 1.22;
} ///:~
注意:访问一个匿名联合的成员就像访问普通的变量一样。唯一的区别在于:该联合的两个变量占用同一内存空间。如果匿名union在文件作用域内,则它必须被声明为static以使它有内部的链接。使用union 的首要目的是为了节省空间。
4、默认参数:默认参数就是在函数声明时就已给定的一个值,如果在调用函数时没有指定这一参数的值,编译器就会自动地插上这个值。
Stash(int size, int initQuantity=0);
现在两个对象的定义:Stash A(100),B(100,0)将会产生完全相同的结果,它们将调用同一个构造函数。对于A,它的第二个参数是由编译器在看到第一个参数是int,而且没有第二个参数时自动加上去的。编译器能看到默认参数。所以它知道应该允许这样的调用,就好像它提供第二个参数一样,而这第二个参数的值就是已经告诉编译器的默认参数。
默认参数的使用规则:1-只有参数列表的后部参数才是可默认的,也就是说,不可以在一个默认参数后面又跟一个非默认的参数。第二,一旦在一个函数调用中开始使用默认参数,那么这个参数后面的所有参赛都必须是默认的。
默认参数只能放在函数声明中。通常在一个头文件中,编译器必须在使用该函数之前知道默认值。
5、void f(int x, int =0, float =1.1);//参数可以没有标识符
C ++ 中,函数定义时,并不一定需要标识符
void f(int x, int , float fit)
{
.......
}
x和fit可以被引用,但中间的这个参数则不行,因为它没有名字。
6、选择重载还是默认参数:Mem(){}默认的构造函数不分配任何的空间。Mem(int SZ){}确保Mem对象中有SZ大小的存储区。
class MyString {
Mem* buf;
public:
MyString();
MyString(char* str);
};
MyString::MyString() { buf = 0; }
MyString::MyString(char* str) {
buf = new Mem(strlen(str) + 1);
strcpy((char*)buf->pointer(), str);
}
默认构造函数设置指针为0.第二个构造函数创建了一个Mem并把一些数据拷贝给它。
使用默认参数:
MyString (char *str="")
MyString::Mystring(char *str){
if(!str){
buf=0;
return ;
}
buf=new Mem(strlen(str)+1);
}
这意味着默认值变成了一个标志:使用非默认值将导致需执行的一块代码被单独分离。这样构造一个小的构造函数,虽然看起来合理,但是一般会导致错误。
使用重载可以维护两个函数的代码,便于维护,使用默认参数是把它们组合成一个函数。
另外:使用默认参数根本不会导致成员函数定义的改变。
7、不能把默认参数作为一个标志去决定执行函数的哪一块,这是基本原则。在这种情况下,只要能够就应该把函数分解成两个或多个重载的函数。
一个默认的参数,应该是一个在一般情况下放在这个位置的值。这个值出现的可能性比其他值要大。默认参数的一个重要应用情况是在开始定义函数时用了一组参数,而使用了一段时间后发现要增加一些参数,通过把这些新增参数都作为默认的参数,就可以保证所有使用这一函数的客户代码不受到影响。
第八章
const 提供了类型检查及安全性。
1、常量概念是为了使程序员能够在变与不变之间画一条界限。
2、常量用法一:值替代 C++可以把const看成是编译期间的常量。
const最初的动机是取代预处理器#define来进行值替代。
预处理器只做一些文本替代,它既没有类型检查概念,也没有类型检查功能,所以预处理器的值替代会产生一些微小的问题,这些问题在C++中可以通过使用const值而避免。
#define BUFSIZE 100
BUFSIZE是一个名字,它只是在预处理期间存在,因此她不占用存储空间且能放在一个头文件里,目的是为了使用它的编译单元提供一个值。BUFSIZE 的工作方式与普通变量类似,而且没有类型信息。
c++用const把值替代带进编译器领域消除这些问题。
const int bufsize =100;
这样就可以在编译时编译器需要知道这个值的任何地方使用bufsize.同时编译器还可以执行常量折叠。即编译器在编译时可以通过必要的计算把一个复杂的常量表达式通过缩减简单化。
char buf[bufsize];
因为预处理器会引入错误,所以我们应该完全用const取代#define的值替代。
3、要使用const而非#define,同样必须把const定义放进头文件里。c++中的const默认为内部链接,也就是说const仅在const被定义过的文件里才是可见的。在链接时不能被其他的编译单元看到,当定义一个const时,必须赋值给它,除非用extern作出了清楚的说明:
extern const int bufsize;
通常c++编译器并不为const创建存储空间,相反它把这个定义保存在它的符号表里,但是extern强制进行了存储空间分配。另外取const的地址也要进行存储空间分配。由于extern意味着使用外部链接,因此必须分配存储空间。也就是说,有几个不同的编译单元应当能够引用它,所以它必须有存储空间。
通常情况下,当extern不是定义的一部分时,不会分配存储空间。如果使用construction,那么编译时会进行常量折叠。
4、const的安全性。
#include <iostream>
using namespace std;
const int i = 100; // Typical constant
const int j = i + 10; // Value from const expr
long address = (long)&j; // Forces storage
char buf[j + 10]; // Still a const expression
int main() {
cout << "type a character & CR:";
const char c = cin.get(); // Can't change
const char c2 = c + 'a';
cout << c2;
// ...
} ///:~
分析: i是编译期间的const,下面一行需要j的地址,所以迫使编译器给j分配存储空间。即使分配了存储空间,把j值保存在程序的某个地方,由于编译器知道j是const,而且知道j值是有效的,因此,这不能妨碍在决定数组buf的大小时使用j.
5、const可以用于集合,但必须保证编译器不会复杂到把一个集合保存到它的符号表中,所以必须分配内存。const意味着:Buneng改变的一块存储空间,然而不能在编译期间使用它的值,因为编译器在编译期间不知道存储的内容。
const int i[] = { 1, 2, 3, 4 };
//! float f[i[3]]; // Illegal
struct S { int i, j; };
const S s[] = { { 1, 2 }, { 3, 4 } };
//! double d[s[1].j]; // Illegal
int main() {} ///:~
编译器提示:是因为它不能在数组定义里找到一个常数表达式。
C中的const常量占用存储空间,而且它的名字是全局符,C++可以把const看成是编译期间的常量。
在C中:
const int bufsize =100;
char buf[bufsize];
编译器报告错误。
C中可以这样写:下面这样写在c++中是不对的,C编译器把它作为声明,指在别的地方有储存分配,因为C默认const 是外部链接的,所以这样是合理的。C++默认const 是内部链接的,
const int bufsize;
c++中可以这样写:
extern const int bufsize;//这行代码也可以在C中。
在c++中是否对const创建内存空间,取决于如何使用。1-定义成extern,2-取const地址。
C++中const默认是内部链接,所以不能在一个文件中定义,在另一个文件中又不可以作为extern来引用。为了使const成为外部链接以便让另一个文件可以对它引用,必须明确地把它定义成extern。
extern const int x=1;
通过初始化并指定extern,强迫给它分配内存,初始化使它成为一个定义而不是一个声明。
C++中:extern const int x;意味着在别处进行了定义。
c++中要求const定义时需要初始化,初始化把定义和声明区别开来。
6、常量用法二:使指针成为const.
使用带有指针的const时,有两个选择:1-const修饰指针正指向的对象。2-const修饰指针里存储的地址。
指向const的指针:指针可变,对象不变。常量指针
const指针:const int *u;//u是一个指针,指向const int.u可以指向任何标识符,因为u 不是一个const,但是它所指的值是不能改变的。
也可以写成:int const * u;
const 指针:使指针本身成为一个const指针。必须把const标明的部分放在*的右边:指针不变,对象可变。指针常量
int d=1;
int *const w=&d;//w是一个指针,指针指向int的const 指针。
指针是const指针,编译器要求给他一个初始值,这个值在指针生命期间不变,然而要改变它所指向的值是可以的。可以写*w=2;
7、可以把一个非const对象的地址赋给一个const指针,然而不能把一个const对象的地址赋给一个非const指针。可以用const_cast类型转换将const转换到非const。
8、字符数组的字面值:char *str="dfd";因为字符数组的字面值是被编译器作为一个常量字符数组建立的,所引用该字符数组得到的结果是它在内存里的首地址。修改该字符数组的任何字符会导致错误。如果想修改字符串:char cp[]="dfddf";
9、常量用法三:用const限定函数参数及返回值。
传递const值:
如果函数按值传递,则可用指定参数是const的。如
void f1(const int i){
i++;//Illegal
}
const:参数不能改变。
返回const值:
const int g();
按值返回,所以这个变量被制成副本,是的初值不会被返回值所修改。
按值返回一个内部类型的时候,应该去掉const.
const int f(){retrn 1;}
当处理用户定义的类型时,按值返回常量是很重要的。如果一个函数按值返回一个类对象为const时,那么这个函数的返回值不能是一个左值。即它不能被赋值,也不能被修改。
class X {
int i;
public:
X(int ii = 0);
void modify();
};
X::X(int ii) { i = ii; }
void X::modify() { i++; }
X f5() {
return X();
}
const X f6() {
return X();
}
void f7(X& x) { // Pass by non-const reference
x.modify();
}
int main() {
f5() = X(1); // OK -- non-const return value
f5().modify(); // OK
//! f7(f5());//cause warning
// Causes compile-time errors:
//! f6() = X(1);
//! f6().modify();
//! f7(f6());
} ///:~
如果一个函数按值返回一个类对象为const时,那么这个函数的返回值不能是一个左值。即它不能被赋值,也不能被修改。
f7()的参数是一个非const引用。这与取一个非const指针一样。会产生一个临时变量。
10、临时变量:临时变量需要存储空间,并且能够被构造和销毁,和变量的区别是看不到它们,编译器负责决定他们的去留以及他们存在的细节,但是关于临时变量:他们自动成为常量。通常接触不到临时变量,改变临时变量是错误的。因为这些信息是不可得的。f7(f5())中,编译器会产生一个临时对象来保持f5()的返回值。使它能传递给f7().如果是按值传递,f7()中形成那个临时量的副本,如果按引用传递,这意味着它取临时对象X的地址,因为f7()所带的参数不是按const引用传递的,所以它允许临时对象X进行修改。但是编译器知道:一旦表达式计算结束。该临时对象不复存在,因此,对临时对象X所作的任何修改也将丢失。
11、把一个临时对象传递给接受const引用的函数是可能的,但不能把一个临时对象传递给接受指针的函数。当临时变量按引用传递给一个函数时,这个函数的参数必须是const引用的原因。
class X {};
X f() { return X(); } // Return by value
void g1(X&) {} // Pass by non-const reference
void g2(const X&) {} // Pass by const reference
int main() {
// Error: const temporary created by f():
//! g1(f());
// OK: g2 takes a const reference:
g2(f());
} ///:~
函数f()按值返回类X的一个对象,当立即取f()的返回值并把它传递给另外一个函数时,将建立一个临时量,该临时量是const,这样,函数g1()中的调用是错误的。
12、常量用法四:类里的const。
在一个类里使用const意味着在这个对象生命期内,它是一个常量。在类里建立一个const时,不能给他初值,参数必须在构造函数里进行。在进入函数主体之前已经被初始化了。所以只能用构造函数初始化列表对const赋值。
class Fred {
const int size;
public:
Fred(int sz);
void print();
};
Fred::Fred(int sz) : size(sz) {}//构造函数初始化列表
void Fred::print() { cout << size << endl; }
int main() {
Fred a(1), b(2), c(3);
a.print(), b.print(), c.print();
}
内部类型的构造函数:使用构造函数初始化列表
class B {
int i;
public:
B(int ii);
void print();
};
B::B(int ii) : i(ii) {}
void B::print() { cout << i << endl; }
注意:这在初始化const数据成员时特别关键,因为它们必须在进入函数体之前被初始化。
把一个内部类型封装在一个类里以保证用构造函数初始化。、
#include <iostream>
using namespace std;
class Integer {
int i;
public:
Integer(int ii = 0);
void print();
};
Integer::Integer(int ii) : i(ii) {}
void Integer::print() { cout << i << ' '; }
int main() {
Integer i[100];
for(int j = 0; j < 100; j++)
i[j].print();
} ///:~
数组Integer数组元素都自动被初始化为零。与for循环和memset相比,这种初始化不必付出更多的开销。编译器可以使用它进行优化。
13、编译期间的常量成员:
如何让一个类有变异期间的常量成员。可以使用static,意味着不管对象被创建多少次,都只有一个实例。
因此,使用static const 可以看成编译期间的常量。必须在static const定义的地方对它初始化。
class StringStack {
static const int size = 100;
const string* stack[size];
int index;
};
StringStack::StringStack() : index(0) {
memset(stack, 0, size * sizeof(string*));
}
14、另一种方案:使用不带实例的enum
一个枚举在编译期间必须有值,它在类中局部出现,而且它的值对于常量表达式是可以用的。
class Bunch {
enum { size = 1000 };
int i[size];
};
int main() {
cout << "sizeof(Bunch) = " << sizeof(Bunch)
<< ", sizeof(i[1000]) = "
<< sizeof(int[1000]) << endl;
} ///:~
//output
sizeof(Bunch) = 4000, sizeof(i[1000]) = 4000
enum不占用存储空间,编译期间得到枚举值,也可以明确地给枚举元素赋值
15、const对象和成员函数
可以用const限定成员函数。用户定义类型和内部类型一样,都可以定义成const对象。
const blob b(2);
对象是const,对象的数据成员在其生命期间不被改变。
如果声明一个成员函数是const,则该成员函数可以被一个const对象所调用,一个没有被明确声明为const的成员函数被看成是将要修改对象中数据成员的函数,而且编译器不允许它被一个const对象调用。
const成员函数。const应放在函数参数表的后面。
class X {
int i;
public:
X(int ii);
int f() const;
void g() {cout<<"g()"<<endl;}
};
class Y{
int i;
public:
Y(int ii):i(ii){cout<<"Y(int ii):i("<<ii<<")"<<endl;}
};
X::X(int ii) : i(ii) {}
int X::f() const {
//error
//! i++;//l-value specifies const object
//error
//! g();//cannot convert 'this' pointer from 'const class X' to 'class X &'
const Y y1(3);//right
Y y2(23); //right
cout<<"f()"<<endl;
return i;
}
int main() {
X x1(10);
const X x2(20);
x1.f();
x2.f();
//error
//! x2.g();//cannot convert 'this' pointer from 'const class X' to 'class X &'
} ///:~
因为f()是一个const成员函数,所以不管它试图以何种方式改变i或者调用另一个非const成员函数,编译器报错。
一个const成员函数调用const和非const对象是安全的。
16、const成员函数中修改数据 const_cast<T *>(this)->i++;或者将数据成员声明为mutable,用以指定特定的数据成员可以在一个const对象里被改变。
class Y {
int i;
mutable int j;
public:
Y();
void f() const;
};
Y::Y() { i = 2; j=5;}
void Y::f() const {
j++;
cout<<"j="<<j<<endl;
cout<<"i="<<i<<endl;
//! i++; // Error -- const member function
((Y*)this)->i++; // OK: cast away const-ness
cout<<"i="<<i<<endl;
// Better: use C++ explicit cast syntax:
(const_cast<Y*>(this))->i++;
cout<<"i="<<i<<endl;
}
int main() {
const Y yy;
yy.f(); // Actually changes it!
} ///:~
17、volatile:在编译器认识的范围之外,这个数据可以被改变。不知何故,环境正在改变数据:可能是多任务、多线程、或者中断处理。
第九章
1、C中保持效率的是宏,C++中保持效率的是内联函数。c++中使用预处理器宏存在两个问题。第一个问题在C也存在:宏看起来像一个函数调用,但并不总是这样。这样就隐藏了难以发现的错误。第二个问题是c++特有的,预处理器不允许访问类的成员数据,这意味着预处理器宏不能用作类的成员函数。
为了保持预处理器宏的效率,又增加安全性,而且还能像一般成员函数一样可以在类里访问自如,C++引入了内联函数。
2、内联函数:在解决c++中访问private类成员的问题过程中,所有与预处理器宏有关的问题也随之排除了。c++中宏的概念是作为内联函数来实现的。内联函数能够像普通函数一样具有我们所有期望的任何行为。
唯一不同之处是内联函数在适当的地方像宏一样展开。所以不需要函数调用的开销。
3、任何在类中定义的函数自动地成为内联函数,但也可以在非类的函数前加上inline关键字使之成为内联函数。为了使之有效,函数体和声明应该结合在一起,否则,编译器将它作为普通函数对待。
inline int plus(int x);没有任何效果,仅仅是声明函数。
inline int plus(int x){}成功的方法。
注意:编译器将检查函数参数列表使用是否正确,并返回值。这是预处理器无法完成的。
4、一般把内联函数放在头文件里。当编译器看到这个定义时,它把函数类型(函数名+返回值)和函数体放在符号表里。
5、类内部的内联函数,函数定义前加inline关键字。任何在类内部定义的函数自动成为内联函数。
#include <iostream>
#include <string>
using namespace std;
class Point {
int i, j, k;
public:
Point(): i(0), j(0), k(0) {}
Point(int ii, int jj, int kk)
: i(ii), j(jj), k(kk) {}
void print(const string& msg = "") const {
if(msg.size() != 0) cout << msg << endl;
cout << "i = " << i << ", "
<< "j = " << j << ", "
<< "k = " << k << endl;
}
};
int main() {
Point p, q(1,2,3);
p.print("value of p");
q.print("value of q");
}
两个构造函数和print函数都默认是内联函数。
使用内联函数的目的是减少函数调用的开销。
内联函数最重要的使用之一是用作访问函数。
class Access {
int i;
public:
int read() const { return i; }
void set(int ii) { i = ii; }
};
int main() {
Access A;
A.set(100);
int x = A.read();
}
在类声明结束后,类中的内联函数才会被计算。内联函数可以向前引用一个还没有声明的函数。
class Forward {
int i;
public:
Forward() : i(0) {}
// Call to undeclared function:
int f() const { return g() + 1; }
int g() const { return i; }
};
int main() {
Forward frwd;
frwd.f();
}
6、宏:有下面三个特征:内联函数不能代替
1-字符串定义 用#指示。把一个标识符转化为字符数组
2-字符串拼接 在当两个相邻的字符串没有分隔符时发生。
1-和2-用于写调试代码:#define DEBUG(X) cout<<#X<<"="<<X<<endl;
下面这句可能会产生问题:#define DEBUG(x) cout<<#x<<endl; x
尤其在for循环中:
for(int i=0;i<100;i++)
DEBUG(f(i));
宏有两句,这样的话只会执行第一句,因此,解决办法是用逗号代替分号。#define DEBUG(x) cout<<#x<<endl, x
3-标志粘贴:将产生一个保存字符数组的标识符和另一个保存字符数组长度的标识符。
#define FIELD(a) char* a##_string; int a##_size
class Record{
FIELD(one);
....
};
第十章
1、函数内部的静态对象,如果在定义一个静态对象时没有指定构造函数参数,这个类就必须有默认的构造函数。
class X {
int i;
public:
X(int ii = 0) : i(ii) {} // Default
~X() { cout << "X::~X()" << endl; }
};
void f() {
static X x1(47);
static X x2; // Default constructor required
}
int main() {
int i=10;
while (i>=0)
{
f();
i--;
}
}
//output
X::~X()
X::~X()
程序第一次调用该函数,执行静态对象构造函数,以后都不需要执行。
2、静态对象的析构函数main函数结尾用exit()来结束程序。用atexit()来指定程序跳出main()时应执行的操作。
class Obj {
char c; // Identifier
public:
Obj(char cc) : c(cc) {
out << "Obj::Obj() for " << c << endl;
}
~Obj() {
out << "Obj::~Obj() for " << c << endl;
}
};
Obj a('a'); // Global (static storage)
// Constructor & destructor always called
void f() {
static Obj b('b');
}
void g() {
static Obj c('c');
}
int main() {
cout << "inside main()" << endl;
f(); // Calls static constructor for b
// g() not called
out << "leaving main()" << endl;
}
全局的类对象的构造函数在main()函数之前就被调用。函数内的静态对象只有在函数被调用的时候才起作用。
3、常量、内联函数在默认情况下都是内部链接的(常量在c++默认情况下是内部链接的,在C默认情况下是外部链接)
所有的全局对象都是隐含为静态存储的。在文件作用域,int a=0;//a被存储在程序的静态存储区。在进入main()之前,a已经初始化了。
static int a=0;只不过改变了变量的可见性。但存储类型没有改变。对象总是主流在静态存储区。
extern void f();和void f();一样。
static void f();在本单元内可见。
4、namespace 名字空间
namespace只能在全局范围内定义,但他们之间可以互相嵌套。
在namespace定义的结尾,右花括号的后面不必加分号。
一个namespace可以在多个头文件中用一个标识符来定义,就好像重定义一个类一样。
一个namespace的名字可以用另一个名字来作它的别名。
namespace BobsSuperDuperLibrary{
}
namespace Bob=BobsSuperDuperLibrary;
在名字空间的类定义中插入一个友元。则函数you()就成了名字空间Me的一个成员。
namespace Me{
class Us{
friend void you();
};
}
5、在一个名字空间引用一个名字可以采取三种方法:1-使用作用域运算符。2-using指令把所有名字引入到名字空间中。3-using声明一次性引用名字。
1-作用域解析
namespace X {
class Y {
static int i;
public:
void f();
};
class Z;
void func();
}
int X::Y::i = 9;
class X::Z {
int u, v, w;
public:
Z(int i);
int g();
};
X::Z::Z(int i) { u = v = w = i; }
int X::Z::g() { return u = v = w = 0; }
void X::func() {
X::Z a(1);
a.g();
}
int main(){}
2-使用指令
namespace Int {
enum sign { positive, negative };
class Integer {
int i;
sign s;
public:
Integer(int ii = 0)
: i(ii),
s(i >= 0 ? positive : negative)
{}
sign getSign() const { return s; }
void setSign(sign sgn) { s = sgn; }
// ...
};
}
using 指令用途之一:把名字空间Int中的所有名字引入到另一个名字空间中。让这些名字嵌套在那个名字空间中
namespace Math {
using namespace Int;
Integer a, b;
Integer divide(Integer, Integer);
// ...
}
可以在一个函数中声明名字空间Int中的所有名字,但是让这些名字嵌套在这个函数中。
#include "NamespaceMath.h"
int main() {
using namespace Math;
Integer a; // Hides Math::a;
a.setSign(negative);
// Now scope resolution is necessary
// to select Math::a :
Math::a.setSign(positive);
} ///:~
3-使用声明 一次性引入名字到当前范围内。
#ifndef USINGDECLARATION_H
#define USINGDECLARATION_H
namespace U {
inline void f() {}
inline void g() {}
}
namespace V {
inline void f() {}
inline void g() {}
}
#endif
#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() {}
4、c++中的静态成员
定义静态数据成员的存储:类的静态数据成员有着单一的存储空间而不管产生了多少个对象,所以存储空间必须在一个单独地方定义。定义必须出现在类的外部,不允许内联,而且只能定义一次,因此通常放在一个类的实现文件中。
class A{
static int i;
public:
....
};
之后,必须在定义文件中为静态数据成员定义存储区。
int A::i =1;
静态成员的初始化表达式是在一个类的作用域内
int x = 100;
class WithStatic {
static int x;
static int y;
public:
void print() const {
cout << "WithStatic::x = " << x << endl;
cout << "WithStatic::y = " << y << endl;
}
};
int WithStatic::x = 1;
int WithStatic::y = x + 1;//=2
// WithStatic::x NOT ::x
不能在局部类中有静态数据成员。
class Outer {
class Inner {
static int i; // OK
};
};
int Outer::Inner::i = 47;
// Local class cannot have static data members:
void f() {
class Local {
public:
//! static int i; // Error
// (How would you define i?)
} x;
}
5、静态成员函数
调用方法:可以用普通的方法调用静态成员函数,用.和箭头->把它与一个对象联系起来。典型的方法是自我调用。不需要任何具体的对象。使用作用域运算符。
class X {
public:
static void f(){};
};
int main() {
X::f();
}
静态成员函数不能访问一般的数据成员,而只能访问静态数据成员,也只能调用其他的静态成员函数。静态成员函数没有this,所以无法访问一般的成员函数。
好处:1-没有传递this所需的额外开销。2-使成员函数在类内的好处。
class X {
int i;
static int j;
public:
X(int ii = 0) : i(ii) {
// Non-static member function can access
// static member function or data:
j = i;
}
int val() const { return i; }
static int incr() {
//! i++; // Error: static member function
// cannot access non-static member data
return ++j;
}
static int f() {
//! val(); // Error: static member function
// cannot access non-static member function
return incr(); // OK -- calls static
}
};
int X::j = 0;
int main() {
X x;
X* xp = &x;
x.f();
xp->f();
X::f(); // Only works with static members
}
因为静态成员函数没有this,所以它既不能访问非静态的数据成员,也不能调用非静态的成员函数。
6、在C++中用到C库。C++通过重载extern来实现。extern后面跟一个字符串来指定想声明的函数的链接类型,后面是函数声明。
使用extern "C" float f(int a, char b);
这告诉编译器f()是C连接,这样就不会转会函数名。
如果有一组替代连接的声明:
extern "C" {
float f(int a, char b);
double d(int a, int b);
}
或在头文件中
extern "C"
{
#include "Myheader.h"
}
第十一章
1、引用就像是能自动被编译器间接引用的常量型指针。
C和C++指针最重要的区别在于C++是一种类型要求更强的语言。C不允许随便把一个类型指针赋值给另一个类型,但允许通过void*实现。
2、c++中的引用,通常用于函数的参数表中和函数的返回值。但也可以独立使用。
使用引用的规则:
1-当引用被创建是必须被初始化。指针则在任何时候被初始化
2-不能有NULL引用,指针可以为NULL。
3-一旦一个引用被初始化为指向一个对象,它就不能改变为另一个对象的引用。指针则可以在任何时候指向另一个对象。
3、引用做参数时,函数内对引用的更改会对函数外的参数产生改变。
int* f(int* x) {
(*x)++;
return x; // Safe, x is outside this scope
}
int& g(int& x) {
x++; // Same effect as in f()
return x; // Safe, outside this scope
}
int& h() {
int q;
//! return q; // Error
static int x;
return x; // Safe, x lives outside this scope
}
int main() {
int a = 0;
f(&a); // Ugly (but explicit)
g(a); // Clean (but hidden)
}
4、常量引用:
void f(int&) {}
void g(const int&) {}
int main() {
//! f(1); // Error
g(1);
}
常量是临时对象,调用f(1)会产生编译期间错误,这是因为编译器必须首先建立一个引用,即编译器为一个int 类型分配存储单元,同时将其初始化为1并为其产生一个地址和引用捆绑在一起。存储的内容必须是常量,
5、指针引用:
C 中:
想改变指针本身而不是他所指的内容:使用void f(int **);
当传递它时,必须取得指针的地址:
int i=47;
int *ip=&i;
f(&ip);
void increment(int** i) { (*i)++; }
int main() {
int* i = 0;
cout << "i = " << i << endl;
increment(&i);
cout << "i = " << i << endl;
}
c++中:
void f(int *&);
void increment(int*& i) { i++; }
int main() {
int* i = 0;
cout << "i = " << i << endl;
increment(i);
cout << "i = " << i << endl;
}
6、拷贝构造函数 X(X&)
编译器在没有提供拷贝构造函数时将会自动地创建。
在C和c++中,参数是从右向左进栈,然后调用函数,调用代码负责清理栈中的参数。
HowMany f(HowMany x) {
x.print("x argument inside f()");
return x;
}
上面通过传值方式传入了对象的拷贝。编译器使用位拷贝。局部对象出了作用域,析构函数就被调用。
拷贝构造函数传递的是源对象的引用。如果设计了拷贝构造函数,使用拷贝构造函数从现有的对象创建新的对象。编译器不使用位拷贝。
class HowMany2 {
string name; // Object identifier
static int objectCount;
public:
HowMany2(const string& id = "") : name(id) {
++objectCount;
print("HowMany2()");
}
~HowMany2() {
--objectCount;
print("~HowMany2()");
}
// The copy-constructor:
HowMany2(const HowMany2& h) : name(h.name) {
name += " copy";
++objectCount;
print("HowMany2(const HowMany2&)");
}
void print(const string& msg = "") const {
if(msg.size() != 0)
out << msg << endl;
out << '\t' << name << ": "
<< "objectCount = "
<< objectCount << endl;
}
};
int HowMany2::objectCount = 0;
// Pass and return BY VALUE:
HowMany2 f(HowMany2 x) {
x.print("x argument inside f()");
out << "Returning from f()" << endl;
return x;
}
int main() {
HowMany2 h("h");
out << "Entering f()" << endl;
HowMany2 h2 = f(h);
h2.print("h2 after call to f()");
out << "Call f(), no return value" << endl;
f(h);
out << "After call to f()" << endl;
}
//output
HowMany2()
h: objectCount = 1
Entering f()
HowMany2(const HowMany2&)
h copy: objectCount = 2
x argument inside f()
h copy: objectCount = 2
Returning from f()
HowMany2(const HowMany2&)
h copy copy: objectCount = 3
~HowMany2()
h copy: objectCount = 2
h2 after call to f()
h copy copy: objectCount = 2
Call f(), no return value
HowMany2(const HowMany2&)
h copy: objectCount = 3
x argument inside f()
h copy: objectCount = 3
Returning from f()
HowMany2(const HowMany2&)
h copy copy: objectCount = 4
~HowMany2()
h copy: objectCount = 3
~HowMany2()
h copy copy: objectCount = 2
After call to f()
~HowMany2()
h copy copy: objectCount = 1
~HowMany2()
h: objectCount = 0
1-h调用普通的构造函数。
2-f(),拷贝构造函数被调用。在f()内创建了一个新对象。它是h的拷贝。
3-f(){return x;}拷入返回值。也就是h2是从现有的对象(在函数f()内的局部变量)创建的。对于h2的标识符,名字变成了h拷贝的拷贝。在对象返回之后,函数结束之前,对象是3.随后h的拷贝被销毁。
4-f()第二次调用,忽略返回值。在参数传入之前拷贝构造函数被调用。返回值调用拷贝构造函数。编译器创建一个临时对象存放函数的返回值。一旦函数调用完结就对内部对象调用析构函数。
7、默认拷贝构造函数,因为拷贝构造函数按值传递方式的参数传递和返回。所以编译器将有效的创建一个默认的拷贝构造函数。如果没有创建拷贝构造函数,c++编译器将自动的创建拷贝构造函数。编译器获得一个拷贝构造函数的过程称为成员方法初始化。
8、仅当准备按值传递的方式传递类对象时,才需要拷贝构造函数,如果不那么做,就不需要拷贝构造函数。
防止按值传递方式传递:声明一个私有拷贝构造函数。甚至不必去定义它,除非成员函数或友元函数需要执行安置传递方式的传递。
class NoCC {
int i;
NoCC(const NoCC&); // No definition
public:
NoCC(int ii = 0) : i(ii) {}
};
void f(NoCC);
int main() {
NoCC n;
//! f(n); // Error: copy-constructor called
//! NoCC n2 = n; // Error: c-c called
//! NoCC n3(n); // Error: c-c called
}
9、
char c;
c=cin.get();
cout<<c<<endl;
char c;
cin.get(c);
cout<<c<<endl;
10、指向成员的指针
考虑有一个指向一个类对象成员的指针。选择一个类中的成员意味着在类中偏移。取得指针指向的内容需要*
对于指向一个对象的指针:语法: ->* 对于一个对象或引用: .*
objPointer->*member=47;
obj.*member=47;
定义:
int ObjClass:: *member;
定义一个名字为members的成员指针,该指针可以指向ObjClass类中的任一int类型的成员。还可以在定义的时候初始化这个成员指针。
int ObjClass:: *member=&ObjClass::a;
指向数据成员的指针:
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();
}
指向成员函数的指针:
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;
}
举例:
class Widget {
public:
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"; }
};
int main() {
Widget w;
Widget* wp = &w;
void (Widget::*pmem)(int) const = &Widget::h;
(w.*pmem)(1);
(wp->*pmem)(2);
}
指针数组:
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);
} ///:~
11、拷贝构造函数采用相同类型的已存在对象的引用作为它的参数,他可以被用来从现有的对象创建新的对象,当用按值传递发那个是传递或返回一个对象时,编译器自动调用这个拷贝构造函数。如果不想通过按值传递方式传递和返回对象,应该创建一个私有 的拷贝构造函数。
第十二章
1、运算符重载
定义重载的运算符就像是定义的函数,只是该函数的名字是operator@,这里@代表了被重载的运算符,函数参数表中参数的个数取决于两个因素:1-运算符是一元的还是二元的。2-运算符被定义为全局函数(对于一元是一个参数,对于二元是两个参数)还是成员函数(对于一元没有参数,对于二元是一个参数,此时可重载的该类的对象用作左侧参数)
class Integer {
int i;
public:
Integer(int ii) : i(ii) {}
const Integer
operator+(const Integer& rv) const {
cout << "operator+" << endl;
return Integer(i + rv.i);
}
Integer&
operator+=(const Integer& rv) {
cout << "operator+=" << endl;
i += rv.i;
return *this;
}
friend ostream& operator <<(ostream & os,Integer rv){
return os<<rv.i<<endl;
}
};
Integer ii(1), jj(2), kk(3);
kk += ii + jj;
2、参数和返回值
1-对于任何函数参数,如果仅需要从参数中读而不改变它,默认的应当作为const引用来传递它。
2-返回值类型取决于运算符的具体含义。如果使用该运算符的结果是产生一个新值,就需要产生一个作为返回值的新对象。如 Integer::operator + 必须生成一个操作数之和的Interger对象。
3-所有赋值运算均改变左值。
4-对于逻辑运算符,需要得到bool返回值。
5-前缀版本:返回值。返回改变后的对象。作为一个引用返回*this。后缀版本返回改变之前的值,所以创建一个代表这个值的独立对象并返回它。
6、返回值优化:通过传值方式返回要创建新对象时,注意使用形式:如operator+: return Integer(left.i+right.i);这是临时对象语法。创建一个临时Integer对象并返回它。
Integer temp(left.i+right.i);
return tremp;
发生三件事:首先:创建temp对象,包括构造函数调用,然后拷贝构造函数把temp拷贝到外部返回值的存储单元里。最后,temp在作用域的结尾时地偶啊用析构函数。
相反,返回临时对象的方式不同,编译器只是返回它,编译器直接把这个对象创建在外部返回值的内存单元,因为不是真正创建一个局部对象。所以仅需要一个普通构造函数的调用,不需要拷贝构造函数调用,也不会调用析构函数。这种方式被称为返回值优化,效率高。
7、不常用的运算符。',',->,->*,[],new,delete.
不能重载的运算符:operator . / operator .* /operator**
8、自动创建operator =,模仿拷贝构造函数的行为,如果类包含对象,对于这些对象,operator=被递归调用。称为成员赋值。
9、自动类型转换:1-特殊类型的构造函数。2-重载的运算符。
1-构造函数转换:构造函数能把另一类型或引用作为它的单个参数。那么这个构造函数允许编译器执行自动类型转换。
class One {
public:
One() {}
};
class Two {
public:
Two(const One&) {}
};
void f(Two) {}
int main() {
One one;
f(one); // Wants a Two, has a One
}
编译器检查f()的声明并注意到它需要一个类Two的对象作为参数。然后编译器检查是否有从对象One到Two的方法。构造函数Two::Two(One)被调用,结果对象Two 被传递给f().
10、阻止构造函数自动转换。explicit,
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
}
f(Two(one))创建一个从类型One到Two的临时对象。
11、运算符转换:第二种自动类型转换的方法是通过运算符重载。类X通过operator Y()将它本身转换到类Y。或者Y(X)构造函数。
class Three {
int i;
public:
Three(int ii = 0, int = 0) : i(ii) {}
};
class Four {
int x;
public:
Four(int xx) : x(xx) {}
operator Three() const { return Three(x); }
};
void g(Three) {}
int main() {
Four four(1);
g(four);
g(1); // Calls Three(1,0)
}
用构造函数技术,目的类执行转换。然而使用运算符技术,是原类执行转换。
12、默认的构造函数、拷贝构造函数、operator=,析构函数都可自动创建。
class Fi {
public:
Fi(){cout<<"6"<<endl;}
~Fi(){cout<<"7"<<endl;}
};
class Fee {
public:
Fee(int) {cout<<"3"<<endl;}
Fee(const Fi&) {cout<<"4"<<endl;}
Fee(const Fee&){cout<<"2"<<endl;}
~Fee(){cout<<"8"<<endl;}
};
class Fo {
int i;
public:
Fo(int x = 0) : i(x) {cout<<"5"<<endl;}
operator Fee() const { cout<<"1"<<endl;return Fee(i); }
~Fo(){
cout<<"9"<<endl;
}
Fo(const Fo&){cout<<"10"<<endl;}
};
int main() {
Fo fo;
Fee fee = fo;//自动类型转换被调用,并创建拷贝构造函数。
}
//output
5
1
3
2
8
8
9
第十三章
1、malloc()和free()是库函数,不在编译器的控制范围之内。new和delete是运算符。编译器可以保证对象的构造函数和析构函数被调用,并且new和delete可以重载。二者分配不成功时返回值不同,前者返回0,后者抛出异常。new 带有内置的长度计算、类型转换、安全检查。
2、创建一个c++对象发生两件事:1-为对象分配内存。2-调用构造函数来初始化那个内存。
3、malloc分配内存:
class Obj {
int i, j, k;
enum { sz = 100 };
char buf[sz];
public:
void initialize() { // Can't use constructor
cout << "initializing Obj" << endl;
i = j = k = 0;
memset(buf, 0, sz);
}
void destroy() const { // Can't use destructor
cout << "destroying Obj" << endl;
}
};
int main() {
Obj* obj = (Obj*)malloc(sizeof(Obj));
require(obj != 0);
obj->initialize();
// ... sometime later:
obj->destroy();
free(obj);
}
由于malloc只分配了一块内存而不是生成一个对象,所以返回了一个void*类型的指针。而C++不允许将一个void*类型指针赋予任何其他指针。用户在使用对象之前必须记住对它初始化。注意构造函数没有被调用,因为构造函数不能被显示的调用。在对象创建时由编译器调用。
4、operator new,分配内存并为这块内存调用构造函数。
MyType *fp=new MyType(1,2);一:调用malloc.二:使用参数表调用构造函数。this指针指向返回的对象的地址。
delete首先调用析构函数。然后释放内存free()。delete 表达式需要一个对象的地址。
如果正在删除的指针是0,则不发生任何事情。经常建议把指针在删除指针后立即把指针赋值为0,避免对它删除两次。
5、delete void指针,唯一发生的事情是释放了内存,但是没有调用析构函数。因此在删除它之前,先进行转换。
void *p=new string;
delete (string *)p;
6、用于数组的new和delete。
MyType *fp=new MyType[100];//在堆上为100个MyType对象分配了足够的内存并为每一个对象调用了构造函数。
fp是一个数组的起始地址。delete []fp;
7、耗尽内存:operator new 找不到足够大的连续内存来安排对象时,new-handler的特殊函数将会被调用。new-handler的默认动作是产生一个异常。<new>中定义new-handler。调用set_new_handler()函数。
#include <iostream>
#include <cstdlib>
#include <new>
using namespace std;
int count = 0;
void out_of_memory() {
cerr << "memory exhausted after " << count
<< " allocations!" << endl;
exit(1);
}
int main() {
set_new_handler(out_of_memory);
while(1) {
count++;
new int[1000]; // Exhausts memory
}
}
new_handler必须不带参数,且返回值为void。内存耗尽时,调用new_handler,new_handler试着调用operator new()。
8、重载的new与delete
new表达式:首先使用operator new 分配内存,然后调用构造函数。delete表达式里,首先调用析构函数,然后调用operator delete。
重载的new必须有size_t参数。它是奇偶uap分配内存的对象的长度。operator new的返回值是void *。做的是分配内存工作。没有完成对象的创建,直到构造函数被调用了才完成对象的创建。operator delete的参数是一个指向由operator new()分配的内存的void *。参数是一个void *。返回值类型void。
void* operator new(size_t sz) {
printf("operator new: %d Bytes\n", sz);
void* m = malloc(sz);
if(!m) puts("out of memory");
return m;
}
void operator delete(void* m) {
puts("operator delete");
free(m);
}
class S {
int i[100];
public:
S() { puts("S::S()"); }
~S() { puts("S::~S()"); }
};
int main() {
puts("creating & destroying an int");
int* p = new int(4);
delete p;
puts("creating & destroying an s");
S* s = new S;
delete s;
puts("creating & destroying S[3]");
S* sa = new S[3];
delete []sa;
}
//output
creating & destroying an int
operator new: 4 Bytes
operator delete
creating & destroying an s
operator new: 400 Bytes
S::S()
S::~S()
operator delete
creating & destroying S[3]
operator new: 1204 Bytes
S::S()
S::S()
S::S()
S::~S()
S::~S()
S::~S()
operator delete
operator delete
operator delete
9、对一个类重载new和delete。尽管不必显示的使用static,但实际上仍是在创建static成员函数。当编译器看到new创建自己定义的类的对象时,它选择成员版本的operator new()而不是全局版本的new().
为数组重载operator new[]和operator delete[].
ofstream trace("ArrayOperatorNew.out");
class Widget {
enum { sz = 10 };
int i[sz];
public:
Widget() { trace << "*"; }
~Widget() { trace << "hua~hua"; }
void* operator new(size_t sz) {
trace << "Widget::new: "
<< sz << " bytes" << endl;
return ::new char[sz];
}
void operator delete(void* p) {
trace << "Widget::delete" << endl;
::delete []p;
}
void* operator new[](size_t sz) {
trace << "Widget::new[]: "
<< sz << " bytes" << endl;
return ::new char[sz];
}
void operator delete[](void* p) {
trace << "Widget::delete[]" << endl;
::delete []p;
}
};
int main() {
trace << "new Widget" << endl;
Widget* w = new Widget;
trace << "\ndelete Widget" << endl;
delete w;
trace << "\nnew Widget[25]" << endl;
Widget* wa = new Widget[25];
trace << "\ndelete []Widget" << endl;
delete []wa;
}
//output
new Widget
Widget::new: 40 bytes
*
delete Widget
hua~huaWidget::delete
new Widget[25]
Widget::new[]: 1004 bytes
*************************
delete []Widget
hua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huaWidget::delete[]
当创建对象数组时,多了4字节,因为4字节是系统用来存放数组信息的。特别是数组中对象的数量。
第十四章
1、组合:在新类中创建已存在的类的对象。新类是由已存在的类的对象组合而成。
继承:创建一个新类作为一个已存在的类的类型,不修改已存在的类,而是采取这个已存在的类的形式,并将代码加入其中。
组合语法:
class Y{
X x;
};
Y y;
y.x.set();
继承语法:
class Y : public X {
int i; // Different from X's i
public:
Y() { i = 0; }
int change() {
i = permute(); // Different name call
return i;
}
void set(int ii) {
i = ii;
X::set(ii); // Same-name function call
}
};
int main() {
cout << "sizeof(X) = " << sizeof(X) << endl;
cout << "sizeof(Y) = "
<< sizeof(Y) << endl;
Y D;
D.change();
// X function interface comes through:
D.read();
D.permute();
// Redefined functions hide base versions:
D.set(12);
} //output:
sizeof(X) = 4
sizeof(Y) = 8
Y 对 X 进行了继承,Y将包含X中的所有数据成员和成员函数,实际上,正如没有对X进行继承,而在Y中创建了一个X的成员对象一样。Y是包含了X的一个子对象。无论是成员对象还是基类存储,都被认为是子对象。派生类可以重定义基类的函数,也可以增加新的函数。
2、构造函数的初始化表达式列表:在进入新类的构造函数体之前调用所有其他的构造函数。
class A {
int i;
public:
A(int ii) : i(ii) {}
~A() {}
void f() const {}
};
class B {
int i;
public:
B(int ii) : i(ii) {}
~B() {}
void f() const {}
};
class C : public B {
A a;
public:
C(int ii) : B(ii), a(ii) {}
~C() {} // Calls ~A() and ~B()
void f() const { // Redefinition
a.f();
B::f();
}
};
int main() {
C c(47);
}
只有通过继承,才能重定义它的函数。而对于成员对象,只能操作这个对象的公共接口而不能重定义它。如果C::f()还没有被定义,则对类型C的一个对象调用f()就不会调用a.f(),而会调用B::f();
3、构造函数与析构函数调用的顺序:
#include <fstream>
using namespace std;
ofstream out("order.out");
#define CLASS(ID) class ID { \
public: \
ID(int) { out << #ID " constructor\n"; } \
~ID() { out << #ID " destructor\n"; } \
};
CLASS(Base1);
CLASS(Member1);
CLASS(Member2);
CLASS(Member3);
CLASS(Member4);
class Derived1 : public Base1 {
Member1 m1;
Member2 m2;
public:
Derived1(int) : m2(1), m1(2), Base1(3) {
out << "Derived1 constructor\n";
}
~Derived1() {
out << "Derived1 destructor\n";
}
};
class Derived2 : public Derived1 {
Member3 m3;
Member4 m4;
public:
Derived2() : m3(1), Derived1(2), m4(3) {
out << "Derived2 constructor\n";
}
~Derived2() {
out << "Derived2 destructor\n";
}
};
int main() {
Derived2 d2;
}
//output:
Base1 constructor
Member1 constructor
Member2 constructor
Derived1 constructor
Member3 constructor
Member4 constructor
Derived2 constructor
Derived2 destructor
Member4 destructor
Member3 destructor
Derived1 destructor
Member2 destructor
Member1 destructor
Base1 destructor
构造函数的调用次序完全不受构造函数的初始化表达式表中的次序影响。该次序是由成员对象在类中声明的次序决定的。
4、名字隐藏:继承一个类,并对它的成员函数重新进行定义。分为两种情况:
1-正如基类中所进行的定义一样,在派生类的定义中明确地定义操作和返回类型,称之为对普通成员函数的重定义。
2-如果基类的成员函数是虚函数的情况,又可以称之为重写。
但是如果在派生类中改变了成员函数的参数列表和返回类型:
任何时候重新定义了基类的一个重载函数,在新类之中所有其他的版本则被自动隐藏了。
class Base {
public:
int f() const {
cout << "Base::f()\n";
return 1;
}
int f(string) const { return 1; }
void g() {}
};
class Derived1 : public Base {
public:
void g() const {}
};
class Derived2 : public Base {
public:
// Redefinition:
int f() const {
cout << "Derived2::f()\n";
return 2;
}
};
class Derived3 : public Base {
public:
// Change return type:
void f() const { cout << "Derived3::f()\n"; }
};
class Derived4 : public Base {
public:
// Change argument list:
int f(int) const {
cout << "Derived4::f()\n";
return 4;
}
};
int main() {
string s("hello");
Derived1 d1;
int x = d1.f();
d1.f(s);
Derived2 d2;
x = d2.f();
// d2.f(s); // string version hidden
Derived3 d3;
//! x = d3.f(); // return int version hidden
Derived4 d4;
//! x = d4.f(); // f() version hidden
x = d4.f(1);
}
//output
Base::f()
Derived2::f()
Derived4::f()
继承的目标是为了实现多态性。如果我们改变了函数特征或返回类型,实际上便改变了基类的接口。如果通过修改基类中一个成员函数的操作与或返回类型来改变了基类的接口,我们就没有使用继承通常所提供的功能,而是按另一种方式来重用该类。
5、非自动继承的函数:不是所有的函数都能自动地从基类继承到派生类中的。构造函数和析构函数用来处理对象的创建和析构操作。构造函数和析构函数不能被继承,必须为每一个特定的派生类分别创建。另外operator =也不能被继承。
继承和静态成员函数:静态成员函数与非静态成员函数的共同点:
1-他们均可以被继承到派生类中
2-如果我们重新定义了一个静态成员,所有在基类中的其他重载函数会被隐藏。
3-如果我们改变了基类中一个函数的特征,所有使用该函数名字的基类版本都将会被隐藏。
然而静态成员函数不可以是虚函数。
6、组合和继承都能把子对象放在新类型中,两个都使用构造函数的初始化表达式表去构造这些子对象。组合通常是在希望新类内部具有已存在类的功能时使用,而不是希望已存在类作为它的接口。就是说,嵌入一个对象,用以实现新类的功能,而新类的用户看到的是新定义的接口,而不是来自老类的接口,为此,在新类的内部嵌入已存在类的private对象。
7、私有继承:创建的新类具有基类的所有数据和功能,但这些功能是隐藏的,所以它只是部分的内部实现。该类的用户访问不到这些内部功能,并且一个对象不能被看做是这个基类的实例。派生类想产生像基类接口一样的接口部分,而不允许该对象的处理像一个基类对象,private继承提供了这个功能。
private继承:私有继承成员公有化:
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继承是有用的。
8、protected 成员:就这个类的用户而言,它是private的,但 它可被从这个类继承来的任何类使用。
9、除了赋值运算符,其他的运算符都可以被继承到派生类中。
10、向上类型转换
继承的最重要的方面不是它为新类提供了成员函数,而是它是基类与新类之间的关系。这种关系可以描述为:新类属于原有类的类型。
向上类型转换:从派生类到基类的类型转换。向上类型转换总是安全的。
如果允许编译器为派生类生成拷贝构造函数,它将首先自动地调用基类的拷贝构造函数,然后再是各成员对象的拷贝构造函数,因此可以得到正确的操作。
class Parent {
int i;
public:
Parent(int ii) : i(ii) {
cout << "Parent(int ii)\n";
}
Parent(const Parent& b) : i(b.i) {
cout << "Parent(const Parent&)\n";
}
Parent() : i(0) { cout << "Parent()\n"; }
friend ostream&
operator<<(ostream& os, const Parent& b) {
return os << "Parent: " << b.i << endl;
}
};
class Member {
int i;
public:
Member(int ii) : i(ii) {
cout << "Member(int ii)\n";
}
Member(const Member& m) : i(m.i) {
cout << "Member(const Member&)\n";
}
friend ostream&
operator<<(ostream& os, const Member& m) {
return os << "Member: " << m.i << endl;
}
};
class Child : public Parent {
int i;
Member m;
public:
Child(int ii) : Parent(ii), i(ii), m(ii) {
cout << "Child(int ii)\n";
}
//Child(const Child& c):i(c.i),m(c.m){cout<<"Child(const Child&)"<<endl;}
friend ostream&
operator<<(ostream& os, const Child& c){
return os << (Parent&)c << c.m
<< "Child: " << c.i << endl;
}
};
int main() {
Child c(2);
cout << "calling copy-constructor: " << endl;
Child c2 = c; // Calls copy-constructor
cout << "values in c2:\n" << c2;
}
//output:编译器为派生类自动生成拷贝构造函数。
Parent(int ii)
Member(int ii)
Child(int ii)
calling copy-constructor:
Parent(const Parent&)
Member(const Member&)
values in c2:
Parent: 2
Member: 2
Child: 2
//output:为派生类自定义拷贝构造函数。
Parent(int ii)
Member(int ii)
Child(int ii)
calling copy-constructor:
Parent()
Member(const Member&)
Child(const Child&)
values in c2:
Parent: 0
Member: 2
Child: 2
Child没有显示定义的拷贝构造函数,编译器将通过调研Parent 和 Member的拷贝构造函数来生成它的拷贝构造函数
为Child自己写拷贝构造函数时,基类部分调用默认的构造函数。这是在没有其他的构造函数可供选择调用的情况下,编译器回溯搜索的结果。
为了解决这个问题:必须记住,无论何时我们在创建了自己的拷贝构造函数时,都要正确地调用基类构造函数。
Child(const Child& c):Parent(c),i(c.i),m(c.m){cout<<"Child(const Child&)"<<endl;}
//output
Parent(int ii)
Member(int ii)
Child(int ii)
calling copy-constructor:
Parent(const Parent&)
Member(const Member&)
Child(const Child&)
values in c2:
Parent: 2
Member: 2
Child: 2
Parent(c)构造函数意味着:因为Child是由Parent继承而来,所以Child的引用也就相当于Parent的引用。基类拷贝构造函数的调用将一个Child的引用向上类型转换为一个Parent的引用,并且使用它来执行拷贝构造函数。
11、指针和引用的向上类型转换
Wind w;
Instrument *ip=&w;//upcast
Instrument &ir=w;//upcast
12、如果想重用已存在类型作为新类型的内部实现的话,我们最好用组合;
如果想使新的类型和基类的类型相同,则应使用继承。如果派生类有基类的接口,它就能向上类型转换到这个基类。
第十五章
1、多态性是面向对象程序设计语言中数据抽象和继承之外的第三个基本特征。
访问控制通过使细节数据设为private,将接口从具体实现中分离开来,多态性提供了接口与具体实现之间的另一层隔离。
2、虚函数增强了类型的概念,而不是只在结构内部隐蔽的封装代码。真正的OOP需要虚函数。
取一个对象的地址(指针或引用),并将其作为基类的地址来处理,称为向上类型转换。
3、虚函数:关键字virtual。仅仅在声明时需要使用关键字,在定义时不需要。如果一个函数在基类中被声明为virtual,那么在所有派生类,它都是virtual的。在派生类中virtual函数的重定义通常称为重写
注意:仅需要在基类中声明一个函数为virtual,调用所有匹配基类声明行为的派生类函数都将使用虚机制。
编译器对每个包含虚函数的类创建一个表。VTABLE。在表中放置特定类的虚函数的地址。在每个带有虚函数的类中,编译器秘密地放置一个指针,VPTR,指向这个对象的VTABLE。当通过虚类指针做虚函数调用时,编译器静态地插入能取得这个VPTR并在VTABLE中查找函数地址的代码。
设置VTABLE、初始化VPTR、为虚函数调用插入代码。
4、不带虚函数,对象的长度是所期望的长度,带有单个虚函数,对象的长度加void*的长度。对于每个VPTR,必须初始化为指向相应的VTABLE的起始地址。
class NoVirtual {
int a;
public:
void x() const {}
int i() const { return 1; }
};
class OneVirtual {
int a;
public:
virtual void x() const {}
int i() const { return 1; }
};
class TwoVirtuals {
int a;
public:
virtual void x() const {}
virtual int i() const { return 1; }
};
int main() {
cout << "int: " << sizeof(int) << endl;
cout << "NoVirtual: "
<< sizeof(NoVirtual) << endl;
cout << "void* : " << sizeof(void*) << endl;
cout << "OneVirtual: "
<< sizeof(OneVirtual) << endl;
cout << "TwoVirtuals: "
<< sizeof(TwoVirtuals) << endl;
}
//output
int: 4
NoVirtual: 4
void* : 4
OneVirtual: 8
TwoVirtuals: 8
5、如果想使用多态,就在每处使用虚函数。
在设计时,希望基类仅仅作为其派生类的一个接口。就是说,仅想对基类进行向上类型转换,使用它的接口,而不希望用户实际地创建一个基类的对象。可以在基类中加入纯虚函数,来使基类成为抽象类。virtual void play()const =0;编译器不允许生成抽象类的对象。
当继承一个抽象类时,必须实现所有的纯虚函数,否则继承出的类也将是一个抽象类。创建一个纯虚函数允许在接口中放置成员函数。抽象类为每个从他派生出来的类创建公共接口。
class Pet {
string pname;
public:
Pet(const string& petName) : pname(petName) {}
virtual string name() const { return pname; }
virtual string speak() const { return ""; }
};
class Dog : public Pet {
string name;
public:
Dog(const string& petName) : Pet(petName) {}
// New virtual function in the Dog class:
virtual string sit() const {
return Pet::name() + " sits";
}
string speak() const { // Override
return Pet::name() + " says 'Bark!'";
}
};
int main() {
Pet* p[] = {new Pet("generic"),new Dog("bob")};
cout << "p[0]->speak() = "
<< p[0]->speak() << endl;
cout << "p[1]->speak() = "
<< p[1]->speak() << endl;
//! cout << "p[1]->sit() = "
//! << p[1]->sit() << endl; // Illegal
}
6、使用多态的目的:让对基类对象操作的代码也能透明的操作派生类对象。
对象切片:对一个对象进行向上类型转换,不使用地址或引用。对象被切片。
7、不能在重新定义的过程中修改虚函数的返回类型。特例:如果返回一个指向基类的指针或引用,则该函数的重新定义版本将会从基类返回的内容中返回一个指向派生类的指针或引用。
8、构造函数是不能为虚函数的,但是析构函数能够且常常是必须是虚函数。这保证派生类对象被正确析构。
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;
}
//output
~Base1()
~Derived2()
~Base2()
9、纯虚析构函数
class AbstractBase {
public:
virtual ~AbstractBase() = 0;
};
AbstractBase::~AbstractBase() {}
class Derived : public AbstractBase {};
一般来说,如果在派生类中基类的纯虚函数没有重新定义,派生类将成为抽象类,然后,如果不进行析构函数定义,编译器将自动为每个类生成一个析构函数的定义。因此编译器会提供定义,并且派生类不会成为抽象类纯虚性的特性是阻止基类的实例化。
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
} ///:~
~Dog()
~Pet()
10、向下类型转换dynamic_cast
class Pet { public: virtual ~Pet(){}};
class Dog : public Pet {};
class Cat : public Pet {};
int main() {
Pet* b = new Cat; // Upcast
// Try to cast it to Dog*:
Dog* d1 = dynamic_cast<Dog*>(b);
// Try to cast it to Cat*:
Cat* d2 = dynamic_cast<Cat*>(b);
cout << "d1 = " << (long)d1 << endl;
cout << "d2 = " << (long)d2 << endl;
}
static_cast静态执行向下类型转换。
第十六章
1、继承和组合提供了重用对象代码的方法,C++模板特征提供了重用源代码的方法。
2、template <class T> 模板语法
template<class T>
class Array {
enum { size = 100 };
T A[size];
public:
T& operator[](int index) {
require(index >= 0 && index < size,
"Index out of range");
return A[index];
}
};
int main() {
Array<int> ia;
Array<float> fa;
for(int i = 0; i < 20; i++) {
ia[i] = i * i;
fa[i] = float(i) * 1.414;
}
for(int j = 0; j < 20; j++)
cout << j << ": " << ia[j]
<< ", " << fa[j] << endl;
}
Array<int> ia;是实例化模板。
非内联函数定义:
template<class T>
class Array {
enum { size = 100 };
T A[size];
public:
T& operator[](int index);
};
template<class T>
T& Array<T>::operator[](int index) {
require(index >= 0 && index < size,
"Index out of range");
return A[index];
}
int main() {
Array<float> fa;
fa[0] = 1.414;
}
3、模板中的常量
template<class T, int size = 100>
class Array {
T array[size];
public:
T& operator[](int index) {
require(index >= 0 && index < size,
"Index out of range");
return array[index];
}
int length() const { return size; }
};
class Number {
float f;
public:
Number(float ff = 0.0f) : f(ff) {}
Number& operator=(const Number& n) {
f = n.f;
return *this;
}
operator float() const { return f; }
friend ostream&
operator<<(ostream& os, const Number& x) {
return os << x.f;
}
};
template<class T, int size = 20>
class Holder {
Array<T, size>* np;
public:
Holder() : np(0) {}
T& operator[](int i) {
require(0 <= i && i < size);
if(!np) np = new Array<T, size>;
return np->operator[](i);
}
int length() const { return size; }
~Holder() { delete np; }
};
int main() {
Holder<Number> h;
for(int i = 0; i < 20; i++)
h[i] = i;
for(int j = 0; j < 20; j++)
cout << h[j] << endl;
}
//output
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
第一章
1、OOP的基本思想:创建抽象的数据类型。
2、类描述了特性(数据元素)和行为(功能)的对象,类实际上是数据类型。
3、向对象发送消息,调用相应的接口函数。
4、访问控制的理由:1-防止客户程序员插手他们不应当接触的部分。2-允许库设计者去改变这个类的内部工作方式,而不必担心这样做会影响客户程序员。public:对所有人都可用。private除了该类型的创建者和该类型的内部成员函数职务,任何人都不可以访问。protected和private相似,只有一点不同,即继承的类可以访问protected成员,但不能访问private成员。
5、OOP的最大优点之一:代码重用。
6、virtual关键字实现晚捆绑。virtual虚函数可用来表示出在相同家族中的类具有不同的行为。这些不同是产生多态行为的原因。把处理派生类型就如同处理其基类型的过程叫向上类型转换
如果一个成员函数是virtual的,当我们给对象发送消息时,这个对象将做正确的事情。
7、OOP的论域:抽象的数据类型、继承、多态。c++采取的方法把效率控制作为最重要的问题。
8、对象的销毁和创建:使用关键字new、delete在堆上动态创建对象
第二章
1、声明和定义:
声明是向编译器介绍名字,标识符,不占存储空间。
定义,建立变量,为名字分配空间。变量和函数可以在不同地方声明,但是只有一个定义。
定义也可以是声明,如果定义int x;之前,编译器没有发现x,编译器则把这一标识符看成是声明,并立即为它分配存储空间。
2、函数声明:int func(int,int);
空参数表:C:表示一个可带任意参数任意类型的函数,这就妨碍了类型检查。在C++中表示不带参数的函数。
变量声明:extern int a;声明一个变量但不定义它。extern也可用于函数的声明。
3、头文件:为了声明在库中已有的函数和变量,用户只需要包含头文件即可。使用#include预处理命令。
#include <header>用尖括号来指定文件时,预处理器是以特定的方式来寻找文件,一般是环境中或编译器命令行指定的某种寻找路径。
#include "local.h" 用双引号,预处理器以“定义实现的路径”来寻找文件。它通常是从当前目录开始寻找,如果文件没有找到,那么include命令就按与尖括号同样的发那个是重新开始寻找。
4、连接器如何查找库:如果连接器在目标模块列表中不能找到函数或变量的定义,它将去查找库,库有多种索引方式,连接器不必到库里查找所有目标模块,而只需浏览索引,当连接器在库中找到定义之后,就将整个目标模块而不仅仅是函数定义链接到可执行程序。注意:仅仅是库中波阿含所需定义的目标模块加入链接,而不是整个库参加链接。
5、#include <cstdlib> system("hello");
第三章
1、数据类型可以是内部的或抽象的,作为一个类,一般被称为抽象数据类型。
系统头文件<limits>中定义了不同的数据类型可能存储的最大值和最小值:、const ulong ULMAX=numeric_limits<ulong>::max();<climits>
2、浮点数的大小等级:float、double、long double。没有long float、也没有short浮点数。
3、char默认是signed. 通过规定signed char,可以强制使用符号位。
4、 int i= int(0);
cout<<i<<endl;
cout<<&i<<endl;
cout<<static_cast<void *>(&i)<<endl;
//output:
0
0012FF7C
0012FF7C
5、通过函数传递指针允许修改外部对象。以引用传递允许一个函数去修改外部对象。指针和引用都有此功能。
6、如果指针声明为void*,它意味着任何类型的地址都可以简介引用那个指针。但是不能使用void引用。
void *vp;
int i;
vp=&i;
一旦间接引用一个void*,就会丢失关于类型的信息。这意味着在使用前必须转换为正确的类型。
int i;
void *vp=&i;
//! *vp=3;//run error,使用前需要转换。
*((int *)vp)=3;
7、作用域规则告诉我们变量的范围:在哪里创建,在哪里销毁。变量的作用域从它的定义点开始,到河定义变量之前最邻近的开括号配对的都一个闭括号。
8、寄存器变量是一个局部变量。不可能得到或计算register变量的地址。register变量只能在一个块中声明,不可能有全局的或静态的register变量。
9、静态变量。初始化只在函数第一次调用时执行。static变量的优点:在函数范围之外它是不可用的。所以他不可能被轻易的改变。static具有文件作用域。在文件的外部不可访问。尽管在文件中声明变量为extern,但是连接器不会找到它。
10、外部变量:extern. extern int i;表示i作为全局变量存在于某处。
11、常量:在C中,如果建立一个常量,必须使用预处理器:#define PI 3.14
c++引入了命名常量的概念,命名常量就像变量。只是它的值不能改变。修饰符const告诉编译器这个名字表示常量。const int x = 10;//=#define x 10
C++中,一个const必须有初始值,dec/oct:0/hex:0x
12、volatile:告诉编译器不知道何时会改变。防止编译器依据变量的稳定性做任何优化。
13、取出一个字节,一位一位输出:
for(int i=7;i>=0;i--)
{
if(val& (1<<7))
{
std::cout<<"1";
}
else
{
std::cout<<"0";
}
14、逗号运算符。int x=a++,b++,v++,c++;//x=c++;在表达式中,只产生最后一个表达式的值。
15、类型转换:
static_cast:用于良性和适度良性转换。包括不用强制转换。dynamic_cast:用于类型安全的向下转换。const_cast常量转换,从const转换到非const,从volatile转换到非volatile。
int i;
long l;
l=static_cast<long>(i);
如果取得了const对象的地址,就可以生成一个指向const的指针,不用转换是不能将它赋值给非const指针的。
const int i=0;
int *j=(int *)&i;//禁止使用
j=const_cast<int *>(&i);
long *l=const_cast<long *>(&i);//error
16、sizeof()是一个运算符,不是函数。typedef命名别名。
typedef 原类型名 别名
struct Structure1{
};
main()
{
struct Structure1 s1;//必须说struct Structure1
...
}
typedef struct{}Structure2;
main()
{
Structure2 s1;
...
}
typedef struct Structure3{
}Structure3;//struct 名字和typedef名字一样。
main()
{
Structure3 s1;
...
}
18、argc>=1,argv[0]程序本身的路径和名字。
<cstdlib> 中定义atoi/atof/atol 。int x=atoi(argv[1])
19、把变量和表达式转换成字符串。#
#define PR(x) cout<<#x<<"="<<x<<"\n";
20、使用assert完成调试后,在#include <cassert>之前插入语句 #define NDEBUG ,则assert()宏失效。
21、函数指针:使用前必须给他赋一个函数的地址,就像一个数组array[10]的地址是由不带方括号的这个数组的名字array一样。函数func()的地址也是由没有参数列表的函数名func产生的,也可以使用更加明显的语法&func().
void func() {
cout << "func() called..." << endl;
}
int main() {
void (*fp)(); // Define a function pointer
fp = func; // Initialize it
(*fp)(); // Dereferencing calls the function
void (*fp2)() = func; // Define and initialize
(*fp2)();
} ///:~
fp = func;使fp获得函数func的地址
第四章
1、动态存储分配,new、delete。new表达式返回指向所请求的转却类型对象的指针。若果声称new type,返回指向type的指针,int *a=new int;delete关键字是new对应的关键字。忘记delete会造成内存泄露。
2、ifstream in("....txt");in.open(",...txt");
3、struct内部可以有成员函数,用作用域解析运算符::.
void Stash::initialize(int size){
}
4、在C中可以赋void*给任何指针,编译器可以通过。
int i=19;
void *vp=&i; //ok in both c and c++
int *ip=vp; //only acceptable in c
但是在c++中,语句不允许。c++运行将任何类型的指针赋给void*可以,但是不允许将void*指针赋给任何其他类型的指针。
5、把函数放进结构中是从C到C++中的根本改变。这使得结构体既可以描写属性,又可以描写行为。这就形成了对象的概念。
在C++中,对象是一块空间,存放着数据而且还隐含着对这些数据进行处理的操作。把函数捆绑在数据结构内部的语言是基于对象的,而不是面向对象的。
把数据和函数捆绑在一起的能力叫做封装。它有属性和行为。object.member(),是对一个对象调用一个成员函数,在面向对象的用那个噶中,称之为向一个对象发送消息。
6、面向对象可以总结为一句话:向对象发送消息。在结构的内部放入函数,结构的种新类型成为抽象数据类型,用这种结构创建的变量称为这个类型的对象或实例。调用对象的成员函数成为向这个对象发送消息,在面向对象的程序设计中断主要动作就是向对象发送消息。
7、一个struct大小是它的所有成员大小的和。当一个struct被编译器处理时,会增大额外的字节以使得边界整齐。使用sizeof(A)...
struct A {
int i[100];
};//400 bytes
struct B {
void f();
};//1 bytes
typedef struct CStashTag {
int size; // Size of each space
int quantity; // Number of storage spaces
int next; // Next empty space
// Dynamically allocated array of bytes:
unsigned char* storage;
} CStash;//16 bytes
struct Stash {
int size; // Size of each space
int quantity; // Number of storage spaces
int next; // Next empty space
// Dynamically allocated array of bytes:
unsigned char* storage;
// Functions!
void initialize(int size);
void cleanup();
int add(const void* element);
void* fetch(int index);
int count();
void inflate(int increase);
}; //16 bytes
8、接口与实现相分离,即声明与成员函数的定义分离。声明放在头文件中。
9、预处理器#define可以用来创建编译时标记。两种选择:1-#define FLAG 告诉编译器这个标记已被定义,但不指定特定的值。2-给它一个值。#define PI 3.14
无论哪种情况,预处理器都能测试该标记,检查它是否已经被定义。#ifdef FLAG 这将得到一个真值,因为上面一行已经定义。
#define 的反义:#undef:1-使得使用相同变量的#ifdef语句得到假值。2-#undef还引起预处理器停止使用宏。
#ifdef的反义#ifndef,标记还没有定义,它得到一个假值。
使用#ifndef可以避免头文件被多次包含。如果头文件被第一次包含,则这个头文件中的内容将被包含到预处理器中。
#ifndef HEARDER_H
#define HEARDER_H
...
#endif
10、不要在头文件中使用using namespace std;如果在头文件中,意味着名字空间将在包含这个头文件的任何文件中消失。
11、全局作用域解析:局部作用域为a,全局作用域为a,要在局部作用域使用全局标识符,使用作用域解析运算符。
int a;
void f() {}
struct S {
int a;
void f();
};
void S::f() {
::f(); // Would be recursive otherwise!
::a++; // Select the global a
a--; // The a at struct scope
}
int main() { S s; f(); } ///:~
S::f() 中没有作用域接卸运算符,编译器默认会选择成员函数的f()和a.
第五章
1、struct访问控制:struct可以有public、protected、private。struct默认是public。
struct A{
public:
...
private:
...
};
继承的结构可以访问protected成员。但不能访问private成员,
5、如果想允许不属于当前结构的一个成员函数访问当前结构中的数据,使用friend友元。friend可以访问private和protected成员。
可以把全局函数声明为friend,也可以把另一个结构中的成员函数甚至整个结构都声明为friend。
struct X;
struct Y {
void f(X*);
};
struct X { // Definition
private:
int i;
public:
void initialize();
friend void g(X*, int); // Global friend 声明的同时,友元
friend void Y::f(X*); // Struct member friend 成员函数在声明为友元之前声明。但要声明Y::f(X*),又必须先声明struct X .因为X*表示引用了对象X的地址,编译器不需要知道对象类型的大小,只需要知道对象的地址。所以struct X;不完全的类型声明即可。否则要必须知道X的全部定义。
friend struct Z; // Entire struct is a friend 不完全的类型说明。
friend void h();
};
void X::initialize() {
i = 0;
}
void g(X* x, int i) {
x->i = i;
}
void Y::f(X* x) {
x->i = 47;
}
struct Z {
private:
int j;
public:
void initialize();
void g(X* x);
};
void Z::initialize() {
j = 99;
}
void Z::g(X* x) {
x->i += j;
}
void h() {
X x;
x.i = 100; // Direct data manipulation
}
6、嵌套友元:嵌套结构不能自动获得访问private成员的权限。需要:1-先声明;2-friend;3-定义这个结构。
//memset(a, 0, sz * sizeof(int));
struct Holder {
private:
int a[sz];
public:
void initialize();
struct Pointer;
friend Pointer;
struct Pointer {
private:
Holder* h;
int* p;
public:
void initialize(Holder* h);
// Move around in the array:
};
};
void Holder::initialize() {
memset(a, 0, sz * sizeof(int));
}
void Holder::Pointer::initialize(Holder* rv) {
h = rv;
p = rv->a;
}
int main() {
Holder h;
Holder::Pointer hp;
h.initialize();
hp.initialize(&h);
} ///:~
7、访问控制通常是指时下细节的隐藏。原因:1-不必担心客户程序员会把内部的数据机制当做他们可使用的接口的一部分来访问。2-将具体实现和接口分离开来。客户程序员只能对public接口发送消息,这样可以改变所有声明为private的成员而不去修改客户程序员的代码。
面向对象编程:同时采用封装和访问控制。
类和struct的每个方面都是一样的,除了class中的成员默认为private,而struct中的成员默认为public。
private中的成员函数属于内部实现的部分,不属于接口部分。
8、将类包含在一个头文件中,修改类时,只需要重编译这个头文件即可,减少项目的重复编译。
第六章
1、c++中的初始化很重要,交给构造函数去完成。编译器在创建对象时自动调用构造函数。class X{public:X(){}};X a;为对象分配内存,构造函数自动被调用。传递到构造函数的第一个参数是this指针,也就是调用这一函数的对象的地址。对于构造函数来说,this指针指向一个米有被初始化的内存块,构造函数的作用就是正确的初始化该内存块。在c++中,对象的定义和初始化是集为一体的,不能只取其中之一。构造函数和析构函数都没有返回值。
2、析构函数不能带任何参数。当对象超出它的作用域时,析构函数由编译器自动调用。
3、集合的初始化:
int b[6]={0};数组大小 SZ = sizeof b / sizeof *b ;
结构也可以这样初始化
typedef struct X{
int x;
float f;
char c;
}X;
X x1={1,2.2,'c'};
构造函数通过被调用来完成初始化。
struct Y{
float f;
int i;
Y(int a):i(a){}
};
Y y1[3]={Y(1),Y(2)};
3、默认构造函数不带任何参数。当struct或class中没有构造函数时,编译器为它自动创建一个,一旦有构造函数而没有默认构造函数,当需要默认构造函数的时候,需要显式地编写默认的构造函数。
第七章
1、重载的思想:同名的函数,但是这些函数的参数列表应该不一样。同名的局部函数(类内部)和全局函数不会发生冲突。
函数重载通过范围和参数来重载。但是不能通过返回值来重载。
因为C中,总是可以调用一个函数但忽略它的返回值。
2、重载的一个重要应用:构造函数。
3、struct、class唯一的不同之处就是struct默认为public,而classs默认为private。struct也可以由够战术和析构函数。另外union也可以由构造函数、析构函数、成员函数甚至访问控制。
union U {
private: // Access control too!
int i;
float f;
public:
U(int a);
U(float b);
~U();
int read_int();
float read_float();
};
U::U(int a) { i = a; }
U::U(float b) { f = b;}
U::~U() { cout << "U::~U()\n"; }
int U::read_int() { return i; }
float U::read_float() { return f; }
union与class的唯一不同之处在于存储数据的方式。union中int和float类型的数据在同一内存中覆盖存放。但是union不能在继承时作为基类使用。
class SuperVar {
enum {
character,
integer,
floating_point
} vartype; // Define one 定义一个enum实例
union { // Anonymous union 匿名联合
char c;
int i;
float f;
};
public:
SuperVar(char ch);
SuperVar(int ii);
SuperVar(float ff);
void print();
};
SuperVar::SuperVar(char ch) {
vartype = character;
c = ch;
}
SuperVar::SuperVar(int ii) {
vartype = integer;
i = ii;
}
SuperVar::SuperVar(float ff) {
vartype = floating_point;
f = ff;
}
void SuperVar::print() {
switch (vartype) {
case character:
cout << "character: " << c << endl;
break;
case integer:
cout << "integer: " << i << endl;
break;
case floating_point:
cout << "float: " << f << endl;
break;
}
}
union没有类型名和标识符,叫做匿名联合,为这个union创建空间但不需要用标识符的方式和以点操作符'.'方式访问这个union的元素。
int main() {
union {
int i;
float f;
};
// Access members without using qualifiers:
i = 12;
f = 1.22;
} ///:~
注意:访问一个匿名联合的成员就像访问普通的变量一样。唯一的区别在于:该联合的两个变量占用同一内存空间。如果匿名union在文件作用域内,则它必须被声明为static以使它有内部的链接。使用union 的首要目的是为了节省空间。
4、默认参数:默认参数就是在函数声明时就已给定的一个值,如果在调用函数时没有指定这一参数的值,编译器就会自动地插上这个值。
Stash(int size, int initQuantity=0);
现在两个对象的定义:Stash A(100),B(100,0)将会产生完全相同的结果,它们将调用同一个构造函数。对于A,它的第二个参数是由编译器在看到第一个参数是int,而且没有第二个参数时自动加上去的。编译器能看到默认参数。所以它知道应该允许这样的调用,就好像它提供第二个参数一样,而这第二个参数的值就是已经告诉编译器的默认参数。
默认参数的使用规则:1-只有参数列表的后部参数才是可默认的,也就是说,不可以在一个默认参数后面又跟一个非默认的参数。第二,一旦在一个函数调用中开始使用默认参数,那么这个参数后面的所有参赛都必须是默认的。
默认参数只能放在函数声明中。通常在一个头文件中,编译器必须在使用该函数之前知道默认值。
5、void f(int x, int =0, float =1.1);//参数可以没有标识符
C ++ 中,函数定义时,并不一定需要标识符
void f(int x, int , float fit)
{
.......
}
x和fit可以被引用,但中间的这个参数则不行,因为它没有名字。
6、选择重载还是默认参数:Mem(){}默认的构造函数不分配任何的空间。Mem(int SZ){}确保Mem对象中有SZ大小的存储区。
class MyString {
Mem* buf;
public:
MyString();
MyString(char* str);
};
MyString::MyString() { buf = 0; }
MyString::MyString(char* str) {
buf = new Mem(strlen(str) + 1);
strcpy((char*)buf->pointer(), str);
}
默认构造函数设置指针为0.第二个构造函数创建了一个Mem并把一些数据拷贝给它。
使用默认参数:
MyString (char *str="")
MyString::Mystring(char *str){
if(!str){
buf=0;
return ;
}
buf=new Mem(strlen(str)+1);
}
这意味着默认值变成了一个标志:使用非默认值将导致需执行的一块代码被单独分离。这样构造一个小的构造函数,虽然看起来合理,但是一般会导致错误。
使用重载可以维护两个函数的代码,便于维护,使用默认参数是把它们组合成一个函数。
另外:使用默认参数根本不会导致成员函数定义的改变。
7、不能把默认参数作为一个标志去决定执行函数的哪一块,这是基本原则。在这种情况下,只要能够就应该把函数分解成两个或多个重载的函数。
一个默认的参数,应该是一个在一般情况下放在这个位置的值。这个值出现的可能性比其他值要大。默认参数的一个重要应用情况是在开始定义函数时用了一组参数,而使用了一段时间后发现要增加一些参数,通过把这些新增参数都作为默认的参数,就可以保证所有使用这一函数的客户代码不受到影响。
第八章
const 提供了类型检查及安全性。
1、常量概念是为了使程序员能够在变与不变之间画一条界限。
2、常量用法一:值替代 C++可以把const看成是编译期间的常量。
const最初的动机是取代预处理器#define来进行值替代。
预处理器只做一些文本替代,它既没有类型检查概念,也没有类型检查功能,所以预处理器的值替代会产生一些微小的问题,这些问题在C++中可以通过使用const值而避免。
#define BUFSIZE 100
BUFSIZE是一个名字,它只是在预处理期间存在,因此她不占用存储空间且能放在一个头文件里,目的是为了使用它的编译单元提供一个值。BUFSIZE 的工作方式与普通变量类似,而且没有类型信息。
c++用const把值替代带进编译器领域消除这些问题。
const int bufsize =100;
这样就可以在编译时编译器需要知道这个值的任何地方使用bufsize.同时编译器还可以执行常量折叠。即编译器在编译时可以通过必要的计算把一个复杂的常量表达式通过缩减简单化。
char buf[bufsize];
因为预处理器会引入错误,所以我们应该完全用const取代#define的值替代。
3、要使用const而非#define,同样必须把const定义放进头文件里。c++中的const默认为内部链接,也就是说const仅在const被定义过的文件里才是可见的。在链接时不能被其他的编译单元看到,当定义一个const时,必须赋值给它,除非用extern作出了清楚的说明:
extern const int bufsize;
通常c++编译器并不为const创建存储空间,相反它把这个定义保存在它的符号表里,但是extern强制进行了存储空间分配。另外取const的地址也要进行存储空间分配。由于extern意味着使用外部链接,因此必须分配存储空间。也就是说,有几个不同的编译单元应当能够引用它,所以它必须有存储空间。
通常情况下,当extern不是定义的一部分时,不会分配存储空间。如果使用construction,那么编译时会进行常量折叠。
4、const的安全性。
#include <iostream>
using namespace std;
const int i = 100; // Typical constant
const int j = i + 10; // Value from const expr
long address = (long)&j; // Forces storage
char buf[j + 10]; // Still a const expression
int main() {
cout << "type a character & CR:";
const char c = cin.get(); // Can't change
const char c2 = c + 'a';
cout << c2;
// ...
} ///:~
分析: i是编译期间的const,下面一行需要j的地址,所以迫使编译器给j分配存储空间。即使分配了存储空间,把j值保存在程序的某个地方,由于编译器知道j是const,而且知道j值是有效的,因此,这不能妨碍在决定数组buf的大小时使用j.
5、const可以用于集合,但必须保证编译器不会复杂到把一个集合保存到它的符号表中,所以必须分配内存。const意味着:Buneng改变的一块存储空间,然而不能在编译期间使用它的值,因为编译器在编译期间不知道存储的内容。
const int i[] = { 1, 2, 3, 4 };
//! float f[i[3]]; // Illegal
struct S { int i, j; };
const S s[] = { { 1, 2 }, { 3, 4 } };
//! double d[s[1].j]; // Illegal
int main() {} ///:~
编译器提示:是因为它不能在数组定义里找到一个常数表达式。
C中的const常量占用存储空间,而且它的名字是全局符,C++可以把const看成是编译期间的常量。
在C中:
const int bufsize =100;
char buf[bufsize];
编译器报告错误。
C中可以这样写:下面这样写在c++中是不对的,C编译器把它作为声明,指在别的地方有储存分配,因为C默认const 是外部链接的,所以这样是合理的。C++默认const 是内部链接的,
const int bufsize;
c++中可以这样写:
extern const int bufsize;//这行代码也可以在C中。
在c++中是否对const创建内存空间,取决于如何使用。1-定义成extern,2-取const地址。
C++中const默认是内部链接,所以不能在一个文件中定义,在另一个文件中又不可以作为extern来引用。为了使const成为外部链接以便让另一个文件可以对它引用,必须明确地把它定义成extern。
extern const int x=1;
通过初始化并指定extern,强迫给它分配内存,初始化使它成为一个定义而不是一个声明。
C++中:extern const int x;意味着在别处进行了定义。
c++中要求const定义时需要初始化,初始化把定义和声明区别开来。
6、常量用法二:使指针成为const.
使用带有指针的const时,有两个选择:1-const修饰指针正指向的对象。2-const修饰指针里存储的地址。
指向const的指针:指针可变,对象不变。常量指针
const指针:const int *u;//u是一个指针,指向const int.u可以指向任何标识符,因为u 不是一个const,但是它所指的值是不能改变的。
也可以写成:int const * u;
const 指针:使指针本身成为一个const指针。必须把const标明的部分放在*的右边:指针不变,对象可变。指针常量
int d=1;
int *const w=&d;//w是一个指针,指针指向int的const 指针。
指针是const指针,编译器要求给他一个初始值,这个值在指针生命期间不变,然而要改变它所指向的值是可以的。可以写*w=2;
7、可以把一个非const对象的地址赋给一个const指针,然而不能把一个const对象的地址赋给一个非const指针。可以用const_cast类型转换将const转换到非const。
8、字符数组的字面值:char *str="dfd";因为字符数组的字面值是被编译器作为一个常量字符数组建立的,所引用该字符数组得到的结果是它在内存里的首地址。修改该字符数组的任何字符会导致错误。如果想修改字符串:char cp[]="dfddf";
9、常量用法三:用const限定函数参数及返回值。
传递const值:
如果函数按值传递,则可用指定参数是const的。如
void f1(const int i){
i++;//Illegal
}
const:参数不能改变。
返回const值:
const int g();
按值返回,所以这个变量被制成副本,是的初值不会被返回值所修改。
按值返回一个内部类型的时候,应该去掉const.
const int f(){retrn 1;}
当处理用户定义的类型时,按值返回常量是很重要的。如果一个函数按值返回一个类对象为const时,那么这个函数的返回值不能是一个左值。即它不能被赋值,也不能被修改。
class X {
int i;
public:
X(int ii = 0);
void modify();
};
X::X(int ii) { i = ii; }
void X::modify() { i++; }
X f5() {
return X();
}
const X f6() {
return X();
}
void f7(X& x) { // Pass by non-const reference
x.modify();
}
int main() {
f5() = X(1); // OK -- non-const return value
f5().modify(); // OK
//! f7(f5());//cause warning
// Causes compile-time errors:
//! f6() = X(1);
//! f6().modify();
//! f7(f6());
} ///:~
如果一个函数按值返回一个类对象为const时,那么这个函数的返回值不能是一个左值。即它不能被赋值,也不能被修改。
f7()的参数是一个非const引用。这与取一个非const指针一样。会产生一个临时变量。
10、临时变量:临时变量需要存储空间,并且能够被构造和销毁,和变量的区别是看不到它们,编译器负责决定他们的去留以及他们存在的细节,但是关于临时变量:他们自动成为常量。通常接触不到临时变量,改变临时变量是错误的。因为这些信息是不可得的。f7(f5())中,编译器会产生一个临时对象来保持f5()的返回值。使它能传递给f7().如果是按值传递,f7()中形成那个临时量的副本,如果按引用传递,这意味着它取临时对象X的地址,因为f7()所带的参数不是按const引用传递的,所以它允许临时对象X进行修改。但是编译器知道:一旦表达式计算结束。该临时对象不复存在,因此,对临时对象X所作的任何修改也将丢失。
11、把一个临时对象传递给接受const引用的函数是可能的,但不能把一个临时对象传递给接受指针的函数。当临时变量按引用传递给一个函数时,这个函数的参数必须是const引用的原因。
class X {};
X f() { return X(); } // Return by value
void g1(X&) {} // Pass by non-const reference
void g2(const X&) {} // Pass by const reference
int main() {
// Error: const temporary created by f():
//! g1(f());
// OK: g2 takes a const reference:
g2(f());
} ///:~
函数f()按值返回类X的一个对象,当立即取f()的返回值并把它传递给另外一个函数时,将建立一个临时量,该临时量是const,这样,函数g1()中的调用是错误的。
12、常量用法四:类里的const。
在一个类里使用const意味着在这个对象生命期内,它是一个常量。在类里建立一个const时,不能给他初值,参数必须在构造函数里进行。在进入函数主体之前已经被初始化了。所以只能用构造函数初始化列表对const赋值。
class Fred {
const int size;
public:
Fred(int sz);
void print();
};
Fred::Fred(int sz) : size(sz) {}//构造函数初始化列表
void Fred::print() { cout << size << endl; }
int main() {
Fred a(1), b(2), c(3);
a.print(), b.print(), c.print();
}
内部类型的构造函数:使用构造函数初始化列表
class B {
int i;
public:
B(int ii);
void print();
};
B::B(int ii) : i(ii) {}
void B::print() { cout << i << endl; }
注意:这在初始化const数据成员时特别关键,因为它们必须在进入函数体之前被初始化。
把一个内部类型封装在一个类里以保证用构造函数初始化。、
#include <iostream>
using namespace std;
class Integer {
int i;
public:
Integer(int ii = 0);
void print();
};
Integer::Integer(int ii) : i(ii) {}
void Integer::print() { cout << i << ' '; }
int main() {
Integer i[100];
for(int j = 0; j < 100; j++)
i[j].print();
} ///:~
数组Integer数组元素都自动被初始化为零。与for循环和memset相比,这种初始化不必付出更多的开销。编译器可以使用它进行优化。
13、编译期间的常量成员:
如何让一个类有变异期间的常量成员。可以使用static,意味着不管对象被创建多少次,都只有一个实例。
因此,使用static const 可以看成编译期间的常量。必须在static const定义的地方对它初始化。
class StringStack {
static const int size = 100;
const string* stack[size];
int index;
};
StringStack::StringStack() : index(0) {
memset(stack, 0, size * sizeof(string*));
}
14、另一种方案:使用不带实例的enum
一个枚举在编译期间必须有值,它在类中局部出现,而且它的值对于常量表达式是可以用的。
class Bunch {
enum { size = 1000 };
int i[size];
};
int main() {
cout << "sizeof(Bunch) = " << sizeof(Bunch)
<< ", sizeof(i[1000]) = "
<< sizeof(int[1000]) << endl;
} ///:~
//output
sizeof(Bunch) = 4000, sizeof(i[1000]) = 4000
enum不占用存储空间,编译期间得到枚举值,也可以明确地给枚举元素赋值
15、const对象和成员函数
可以用const限定成员函数。用户定义类型和内部类型一样,都可以定义成const对象。
const blob b(2);
对象是const,对象的数据成员在其生命期间不被改变。
如果声明一个成员函数是const,则该成员函数可以被一个const对象所调用,一个没有被明确声明为const的成员函数被看成是将要修改对象中数据成员的函数,而且编译器不允许它被一个const对象调用。
const成员函数。const应放在函数参数表的后面。
class X {
int i;
public:
X(int ii);
int f() const;
void g() {cout<<"g()"<<endl;}
};
class Y{
int i;
public:
Y(int ii):i(ii){cout<<"Y(int ii):i("<<ii<<")"<<endl;}
};
X::X(int ii) : i(ii) {}
int X::f() const {
//error
//! i++;//l-value specifies const object
//error
//! g();//cannot convert 'this' pointer from 'const class X' to 'class X &'
const Y y1(3);//right
Y y2(23); //right
cout<<"f()"<<endl;
return i;
}
int main() {
X x1(10);
const X x2(20);
x1.f();
x2.f();
//error
//! x2.g();//cannot convert 'this' pointer from 'const class X' to 'class X &'
} ///:~
因为f()是一个const成员函数,所以不管它试图以何种方式改变i或者调用另一个非const成员函数,编译器报错。
一个const成员函数调用const和非const对象是安全的。
16、const成员函数中修改数据 const_cast<T *>(this)->i++;或者将数据成员声明为mutable,用以指定特定的数据成员可以在一个const对象里被改变。
class Y {
int i;
mutable int j;
public:
Y();
void f() const;
};
Y::Y() { i = 2; j=5;}
void Y::f() const {
j++;
cout<<"j="<<j<<endl;
cout<<"i="<<i<<endl;
//! i++; // Error -- const member function
((Y*)this)->i++; // OK: cast away const-ness
cout<<"i="<<i<<endl;
// Better: use C++ explicit cast syntax:
(const_cast<Y*>(this))->i++;
cout<<"i="<<i<<endl;
}
int main() {
const Y yy;
yy.f(); // Actually changes it!
} ///:~
17、volatile:在编译器认识的范围之外,这个数据可以被改变。不知何故,环境正在改变数据:可能是多任务、多线程、或者中断处理。
第九章
1、C中保持效率的是宏,C++中保持效率的是内联函数。c++中使用预处理器宏存在两个问题。第一个问题在C也存在:宏看起来像一个函数调用,但并不总是这样。这样就隐藏了难以发现的错误。第二个问题是c++特有的,预处理器不允许访问类的成员数据,这意味着预处理器宏不能用作类的成员函数。
为了保持预处理器宏的效率,又增加安全性,而且还能像一般成员函数一样可以在类里访问自如,C++引入了内联函数。
2、内联函数:在解决c++中访问private类成员的问题过程中,所有与预处理器宏有关的问题也随之排除了。c++中宏的概念是作为内联函数来实现的。内联函数能够像普通函数一样具有我们所有期望的任何行为。
唯一不同之处是内联函数在适当的地方像宏一样展开。所以不需要函数调用的开销。
3、任何在类中定义的函数自动地成为内联函数,但也可以在非类的函数前加上inline关键字使之成为内联函数。为了使之有效,函数体和声明应该结合在一起,否则,编译器将它作为普通函数对待。
inline int plus(int x);没有任何效果,仅仅是声明函数。
inline int plus(int x){}成功的方法。
注意:编译器将检查函数参数列表使用是否正确,并返回值。这是预处理器无法完成的。
4、一般把内联函数放在头文件里。当编译器看到这个定义时,它把函数类型(函数名+返回值)和函数体放在符号表里。
5、类内部的内联函数,函数定义前加inline关键字。任何在类内部定义的函数自动成为内联函数。
#include <iostream>
#include <string>
using namespace std;
class Point {
int i, j, k;
public:
Point(): i(0), j(0), k(0) {}
Point(int ii, int jj, int kk)
: i(ii), j(jj), k(kk) {}
void print(const string& msg = "") const {
if(msg.size() != 0) cout << msg << endl;
cout << "i = " << i << ", "
<< "j = " << j << ", "
<< "k = " << k << endl;
}
};
int main() {
Point p, q(1,2,3);
p.print("value of p");
q.print("value of q");
}
两个构造函数和print函数都默认是内联函数。
使用内联函数的目的是减少函数调用的开销。
内联函数最重要的使用之一是用作访问函数。
class Access {
int i;
public:
int read() const { return i; }
void set(int ii) { i = ii; }
};
int main() {
Access A;
A.set(100);
int x = A.read();
}
在类声明结束后,类中的内联函数才会被计算。内联函数可以向前引用一个还没有声明的函数。
class Forward {
int i;
public:
Forward() : i(0) {}
// Call to undeclared function:
int f() const { return g() + 1; }
int g() const { return i; }
};
int main() {
Forward frwd;
frwd.f();
}
6、宏:有下面三个特征:内联函数不能代替
1-字符串定义 用#指示。把一个标识符转化为字符数组
2-字符串拼接 在当两个相邻的字符串没有分隔符时发生。
1-和2-用于写调试代码:#define DEBUG(X) cout<<#X<<"="<<X<<endl;
下面这句可能会产生问题:#define DEBUG(x) cout<<#x<<endl; x
尤其在for循环中:
for(int i=0;i<100;i++)
DEBUG(f(i));
宏有两句,这样的话只会执行第一句,因此,解决办法是用逗号代替分号。#define DEBUG(x) cout<<#x<<endl, x
3-标志粘贴:将产生一个保存字符数组的标识符和另一个保存字符数组长度的标识符。
#define FIELD(a) char* a##_string; int a##_size
class Record{
FIELD(one);
....
};
第十章
1、函数内部的静态对象,如果在定义一个静态对象时没有指定构造函数参数,这个类就必须有默认的构造函数。
class X {
int i;
public:
X(int ii = 0) : i(ii) {} // Default
~X() { cout << "X::~X()" << endl; }
};
void f() {
static X x1(47);
static X x2; // Default constructor required
}
int main() {
int i=10;
while (i>=0)
{
f();
i--;
}
}
//output
X::~X()
X::~X()
程序第一次调用该函数,执行静态对象构造函数,以后都不需要执行。
2、静态对象的析构函数main函数结尾用exit()来结束程序。用atexit()来指定程序跳出main()时应执行的操作。
class Obj {
char c; // Identifier
public:
Obj(char cc) : c(cc) {
out << "Obj::Obj() for " << c << endl;
}
~Obj() {
out << "Obj::~Obj() for " << c << endl;
}
};
Obj a('a'); // Global (static storage)
// Constructor & destructor always called
void f() {
static Obj b('b');
}
void g() {
static Obj c('c');
}
int main() {
cout << "inside main()" << endl;
f(); // Calls static constructor for b
// g() not called
out << "leaving main()" << endl;
}
全局的类对象的构造函数在main()函数之前就被调用。函数内的静态对象只有在函数被调用的时候才起作用。
3、常量、内联函数在默认情况下都是内部链接的(常量在c++默认情况下是内部链接的,在C默认情况下是外部链接)
所有的全局对象都是隐含为静态存储的。在文件作用域,int a=0;//a被存储在程序的静态存储区。在进入main()之前,a已经初始化了。
static int a=0;只不过改变了变量的可见性。但存储类型没有改变。对象总是主流在静态存储区。
extern void f();和void f();一样。
static void f();在本单元内可见。
4、namespace 名字空间
namespace只能在全局范围内定义,但他们之间可以互相嵌套。
在namespace定义的结尾,右花括号的后面不必加分号。
一个namespace可以在多个头文件中用一个标识符来定义,就好像重定义一个类一样。
一个namespace的名字可以用另一个名字来作它的别名。
namespace BobsSuperDuperLibrary{
}
namespace Bob=BobsSuperDuperLibrary;
在名字空间的类定义中插入一个友元。则函数you()就成了名字空间Me的一个成员。
namespace Me{
class Us{
friend void you();
};
}
5、在一个名字空间引用一个名字可以采取三种方法:1-使用作用域运算符。2-using指令把所有名字引入到名字空间中。3-using声明一次性引用名字。
1-作用域解析
namespace X {
class Y {
static int i;
public:
void f();
};
class Z;
void func();
}
int X::Y::i = 9;
class X::Z {
int u, v, w;
public:
Z(int i);
int g();
};
X::Z::Z(int i) { u = v = w = i; }
int X::Z::g() { return u = v = w = 0; }
void X::func() {
X::Z a(1);
a.g();
}
int main(){}
2-使用指令
namespace Int {
enum sign { positive, negative };
class Integer {
int i;
sign s;
public:
Integer(int ii = 0)
: i(ii),
s(i >= 0 ? positive : negative)
{}
sign getSign() const { return s; }
void setSign(sign sgn) { s = sgn; }
// ...
};
}
using 指令用途之一:把名字空间Int中的所有名字引入到另一个名字空间中。让这些名字嵌套在那个名字空间中
namespace Math {
using namespace Int;
Integer a, b;
Integer divide(Integer, Integer);
// ...
}
可以在一个函数中声明名字空间Int中的所有名字,但是让这些名字嵌套在这个函数中。
#include "NamespaceMath.h"
int main() {
using namespace Math;
Integer a; // Hides Math::a;
a.setSign(negative);
// Now scope resolution is necessary
// to select Math::a :
Math::a.setSign(positive);
} ///:~
3-使用声明 一次性引入名字到当前范围内。
#ifndef USINGDECLARATION_H
#define USINGDECLARATION_H
namespace U {
inline void f() {}
inline void g() {}
}
namespace V {
inline void f() {}
inline void g() {}
}
#endif
#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() {}
4、c++中的静态成员
定义静态数据成员的存储:类的静态数据成员有着单一的存储空间而不管产生了多少个对象,所以存储空间必须在一个单独地方定义。定义必须出现在类的外部,不允许内联,而且只能定义一次,因此通常放在一个类的实现文件中。
class A{
static int i;
public:
....
};
之后,必须在定义文件中为静态数据成员定义存储区。
int A::i =1;
静态成员的初始化表达式是在一个类的作用域内
int x = 100;
class WithStatic {
static int x;
static int y;
public:
void print() const {
cout << "WithStatic::x = " << x << endl;
cout << "WithStatic::y = " << y << endl;
}
};
int WithStatic::x = 1;
int WithStatic::y = x + 1;//=2
// WithStatic::x NOT ::x
不能在局部类中有静态数据成员。
class Outer {
class Inner {
static int i; // OK
};
};
int Outer::Inner::i = 47;
// Local class cannot have static data members:
void f() {
class Local {
public:
//! static int i; // Error
// (How would you define i?)
} x;
}
5、静态成员函数
调用方法:可以用普通的方法调用静态成员函数,用.和箭头->把它与一个对象联系起来。典型的方法是自我调用。不需要任何具体的对象。使用作用域运算符。
class X {
public:
static void f(){};
};
int main() {
X::f();
}
静态成员函数不能访问一般的数据成员,而只能访问静态数据成员,也只能调用其他的静态成员函数。静态成员函数没有this,所以无法访问一般的成员函数。
好处:1-没有传递this所需的额外开销。2-使成员函数在类内的好处。
class X {
int i;
static int j;
public:
X(int ii = 0) : i(ii) {
// Non-static member function can access
// static member function or data:
j = i;
}
int val() const { return i; }
static int incr() {
//! i++; // Error: static member function
// cannot access non-static member data
return ++j;
}
static int f() {
//! val(); // Error: static member function
// cannot access non-static member function
return incr(); // OK -- calls static
}
};
int X::j = 0;
int main() {
X x;
X* xp = &x;
x.f();
xp->f();
X::f(); // Only works with static members
}
因为静态成员函数没有this,所以它既不能访问非静态的数据成员,也不能调用非静态的成员函数。
6、在C++中用到C库。C++通过重载extern来实现。extern后面跟一个字符串来指定想声明的函数的链接类型,后面是函数声明。
使用extern "C" float f(int a, char b);
这告诉编译器f()是C连接,这样就不会转会函数名。
如果有一组替代连接的声明:
extern "C" {
float f(int a, char b);
double d(int a, int b);
}
或在头文件中
extern "C"
{
#include "Myheader.h"
}
第十一章
1、引用就像是能自动被编译器间接引用的常量型指针。
C和C++指针最重要的区别在于C++是一种类型要求更强的语言。C不允许随便把一个类型指针赋值给另一个类型,但允许通过void*实现。
2、c++中的引用,通常用于函数的参数表中和函数的返回值。但也可以独立使用。
使用引用的规则:
1-当引用被创建是必须被初始化。指针则在任何时候被初始化
2-不能有NULL引用,指针可以为NULL。
3-一旦一个引用被初始化为指向一个对象,它就不能改变为另一个对象的引用。指针则可以在任何时候指向另一个对象。
3、引用做参数时,函数内对引用的更改会对函数外的参数产生改变。
int* f(int* x) {
(*x)++;
return x; // Safe, x is outside this scope
}
int& g(int& x) {
x++; // Same effect as in f()
return x; // Safe, outside this scope
}
int& h() {
int q;
//! return q; // Error
static int x;
return x; // Safe, x lives outside this scope
}
int main() {
int a = 0;
f(&a); // Ugly (but explicit)
g(a); // Clean (but hidden)
}
4、常量引用:
void f(int&) {}
void g(const int&) {}
int main() {
//! f(1); // Error
g(1);
}
常量是临时对象,调用f(1)会产生编译期间错误,这是因为编译器必须首先建立一个引用,即编译器为一个int 类型分配存储单元,同时将其初始化为1并为其产生一个地址和引用捆绑在一起。存储的内容必须是常量,
5、指针引用:
C 中:
想改变指针本身而不是他所指的内容:使用void f(int **);
当传递它时,必须取得指针的地址:
int i=47;
int *ip=&i;
f(&ip);
void increment(int** i) { (*i)++; }
int main() {
int* i = 0;
cout << "i = " << i << endl;
increment(&i);
cout << "i = " << i << endl;
}
c++中:
void f(int *&);
void increment(int*& i) { i++; }
int main() {
int* i = 0;
cout << "i = " << i << endl;
increment(i);
cout << "i = " << i << endl;
}
6、拷贝构造函数 X(X&)
编译器在没有提供拷贝构造函数时将会自动地创建。
在C和c++中,参数是从右向左进栈,然后调用函数,调用代码负责清理栈中的参数。
HowMany f(HowMany x) {
x.print("x argument inside f()");
return x;
}
上面通过传值方式传入了对象的拷贝。编译器使用位拷贝。局部对象出了作用域,析构函数就被调用。
拷贝构造函数传递的是源对象的引用。如果设计了拷贝构造函数,使用拷贝构造函数从现有的对象创建新的对象。编译器不使用位拷贝。
class HowMany2 {
string name; // Object identifier
static int objectCount;
public:
HowMany2(const string& id = "") : name(id) {
++objectCount;
print("HowMany2()");
}
~HowMany2() {
--objectCount;
print("~HowMany2()");
}
// The copy-constructor:
HowMany2(const HowMany2& h) : name(h.name) {
name += " copy";
++objectCount;
print("HowMany2(const HowMany2&)");
}
void print(const string& msg = "") const {
if(msg.size() != 0)
out << msg << endl;
out << '\t' << name << ": "
<< "objectCount = "
<< objectCount << endl;
}
};
int HowMany2::objectCount = 0;
// Pass and return BY VALUE:
HowMany2 f(HowMany2 x) {
x.print("x argument inside f()");
out << "Returning from f()" << endl;
return x;
}
int main() {
HowMany2 h("h");
out << "Entering f()" << endl;
HowMany2 h2 = f(h);
h2.print("h2 after call to f()");
out << "Call f(), no return value" << endl;
f(h);
out << "After call to f()" << endl;
}
//output
HowMany2()
h: objectCount = 1
Entering f()
HowMany2(const HowMany2&)
h copy: objectCount = 2
x argument inside f()
h copy: objectCount = 2
Returning from f()
HowMany2(const HowMany2&)
h copy copy: objectCount = 3
~HowMany2()
h copy: objectCount = 2
h2 after call to f()
h copy copy: objectCount = 2
Call f(), no return value
HowMany2(const HowMany2&)
h copy: objectCount = 3
x argument inside f()
h copy: objectCount = 3
Returning from f()
HowMany2(const HowMany2&)
h copy copy: objectCount = 4
~HowMany2()
h copy: objectCount = 3
~HowMany2()
h copy copy: objectCount = 2
After call to f()
~HowMany2()
h copy copy: objectCount = 1
~HowMany2()
h: objectCount = 0
1-h调用普通的构造函数。
2-f(),拷贝构造函数被调用。在f()内创建了一个新对象。它是h的拷贝。
3-f(){return x;}拷入返回值。也就是h2是从现有的对象(在函数f()内的局部变量)创建的。对于h2的标识符,名字变成了h拷贝的拷贝。在对象返回之后,函数结束之前,对象是3.随后h的拷贝被销毁。
4-f()第二次调用,忽略返回值。在参数传入之前拷贝构造函数被调用。返回值调用拷贝构造函数。编译器创建一个临时对象存放函数的返回值。一旦函数调用完结就对内部对象调用析构函数。
7、默认拷贝构造函数,因为拷贝构造函数按值传递方式的参数传递和返回。所以编译器将有效的创建一个默认的拷贝构造函数。如果没有创建拷贝构造函数,c++编译器将自动的创建拷贝构造函数。编译器获得一个拷贝构造函数的过程称为成员方法初始化。
8、仅当准备按值传递的方式传递类对象时,才需要拷贝构造函数,如果不那么做,就不需要拷贝构造函数。
防止按值传递方式传递:声明一个私有拷贝构造函数。甚至不必去定义它,除非成员函数或友元函数需要执行安置传递方式的传递。
class NoCC {
int i;
NoCC(const NoCC&); // No definition
public:
NoCC(int ii = 0) : i(ii) {}
};
void f(NoCC);
int main() {
NoCC n;
//! f(n); // Error: copy-constructor called
//! NoCC n2 = n; // Error: c-c called
//! NoCC n3(n); // Error: c-c called
}
9、
char c;
c=cin.get();
cout<<c<<endl;
char c;
cin.get(c);
cout<<c<<endl;
10、指向成员的指针
考虑有一个指向一个类对象成员的指针。选择一个类中的成员意味着在类中偏移。取得指针指向的内容需要*
对于指向一个对象的指针:语法: ->* 对于一个对象或引用: .*
objPointer->*member=47;
obj.*member=47;
定义:
int ObjClass:: *member;
定义一个名字为members的成员指针,该指针可以指向ObjClass类中的任一int类型的成员。还可以在定义的时候初始化这个成员指针。
int ObjClass:: *member=&ObjClass::a;
指向数据成员的指针:
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();
}
指向成员函数的指针:
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;
}
举例:
class Widget {
public:
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"; }
};
int main() {
Widget w;
Widget* wp = &w;
void (Widget::*pmem)(int) const = &Widget::h;
(w.*pmem)(1);
(wp->*pmem)(2);
}
指针数组:
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);
} ///:~
11、拷贝构造函数采用相同类型的已存在对象的引用作为它的参数,他可以被用来从现有的对象创建新的对象,当用按值传递发那个是传递或返回一个对象时,编译器自动调用这个拷贝构造函数。如果不想通过按值传递方式传递和返回对象,应该创建一个私有 的拷贝构造函数。
第十二章
1、运算符重载
定义重载的运算符就像是定义的函数,只是该函数的名字是operator@,这里@代表了被重载的运算符,函数参数表中参数的个数取决于两个因素:1-运算符是一元的还是二元的。2-运算符被定义为全局函数(对于一元是一个参数,对于二元是两个参数)还是成员函数(对于一元没有参数,对于二元是一个参数,此时可重载的该类的对象用作左侧参数)
class Integer {
int i;
public:
Integer(int ii) : i(ii) {}
const Integer
operator+(const Integer& rv) const {
cout << "operator+" << endl;
return Integer(i + rv.i);
}
Integer&
operator+=(const Integer& rv) {
cout << "operator+=" << endl;
i += rv.i;
return *this;
}
friend ostream& operator <<(ostream & os,Integer rv){
return os<<rv.i<<endl;
}
};
Integer ii(1), jj(2), kk(3);
kk += ii + jj;
2、参数和返回值
1-对于任何函数参数,如果仅需要从参数中读而不改变它,默认的应当作为const引用来传递它。
2-返回值类型取决于运算符的具体含义。如果使用该运算符的结果是产生一个新值,就需要产生一个作为返回值的新对象。如 Integer::operator + 必须生成一个操作数之和的Interger对象。
3-所有赋值运算均改变左值。
4-对于逻辑运算符,需要得到bool返回值。
5-前缀版本:返回值。返回改变后的对象。作为一个引用返回*this。后缀版本返回改变之前的值,所以创建一个代表这个值的独立对象并返回它。
6、返回值优化:通过传值方式返回要创建新对象时,注意使用形式:如operator+: return Integer(left.i+right.i);这是临时对象语法。创建一个临时Integer对象并返回它。
Integer temp(left.i+right.i);
return tremp;
发生三件事:首先:创建temp对象,包括构造函数调用,然后拷贝构造函数把temp拷贝到外部返回值的存储单元里。最后,temp在作用域的结尾时地偶啊用析构函数。
相反,返回临时对象的方式不同,编译器只是返回它,编译器直接把这个对象创建在外部返回值的内存单元,因为不是真正创建一个局部对象。所以仅需要一个普通构造函数的调用,不需要拷贝构造函数调用,也不会调用析构函数。这种方式被称为返回值优化,效率高。
7、不常用的运算符。',',->,->*,[],new,delete.
不能重载的运算符:operator . / operator .* /operator**
8、自动创建operator =,模仿拷贝构造函数的行为,如果类包含对象,对于这些对象,operator=被递归调用。称为成员赋值。
9、自动类型转换:1-特殊类型的构造函数。2-重载的运算符。
1-构造函数转换:构造函数能把另一类型或引用作为它的单个参数。那么这个构造函数允许编译器执行自动类型转换。
class One {
public:
One() {}
};
class Two {
public:
Two(const One&) {}
};
void f(Two) {}
int main() {
One one;
f(one); // Wants a Two, has a One
}
编译器检查f()的声明并注意到它需要一个类Two的对象作为参数。然后编译器检查是否有从对象One到Two的方法。构造函数Two::Two(One)被调用,结果对象Two 被传递给f().
10、阻止构造函数自动转换。explicit,
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
}
f(Two(one))创建一个从类型One到Two的临时对象。
11、运算符转换:第二种自动类型转换的方法是通过运算符重载。类X通过operator Y()将它本身转换到类Y。或者Y(X)构造函数。
class Three {
int i;
public:
Three(int ii = 0, int = 0) : i(ii) {}
};
class Four {
int x;
public:
Four(int xx) : x(xx) {}
operator Three() const { return Three(x); }
};
void g(Three) {}
int main() {
Four four(1);
g(four);
g(1); // Calls Three(1,0)
}
用构造函数技术,目的类执行转换。然而使用运算符技术,是原类执行转换。
12、默认的构造函数、拷贝构造函数、operator=,析构函数都可自动创建。
class Fi {
public:
Fi(){cout<<"6"<<endl;}
~Fi(){cout<<"7"<<endl;}
};
class Fee {
public:
Fee(int) {cout<<"3"<<endl;}
Fee(const Fi&) {cout<<"4"<<endl;}
Fee(const Fee&){cout<<"2"<<endl;}
~Fee(){cout<<"8"<<endl;}
};
class Fo {
int i;
public:
Fo(int x = 0) : i(x) {cout<<"5"<<endl;}
operator Fee() const { cout<<"1"<<endl;return Fee(i); }
~Fo(){
cout<<"9"<<endl;
}
Fo(const Fo&){cout<<"10"<<endl;}
};
int main() {
Fo fo;
Fee fee = fo;//自动类型转换被调用,并创建拷贝构造函数。
}
//output
5
1
3
2
8
8
9
第十三章
1、malloc()和free()是库函数,不在编译器的控制范围之内。new和delete是运算符。编译器可以保证对象的构造函数和析构函数被调用,并且new和delete可以重载。二者分配不成功时返回值不同,前者返回0,后者抛出异常。new 带有内置的长度计算、类型转换、安全检查。
2、创建一个c++对象发生两件事:1-为对象分配内存。2-调用构造函数来初始化那个内存。
3、malloc分配内存:
class Obj {
int i, j, k;
enum { sz = 100 };
char buf[sz];
public:
void initialize() { // Can't use constructor
cout << "initializing Obj" << endl;
i = j = k = 0;
memset(buf, 0, sz);
}
void destroy() const { // Can't use destructor
cout << "destroying Obj" << endl;
}
};
int main() {
Obj* obj = (Obj*)malloc(sizeof(Obj));
require(obj != 0);
obj->initialize();
// ... sometime later:
obj->destroy();
free(obj);
}
由于malloc只分配了一块内存而不是生成一个对象,所以返回了一个void*类型的指针。而C++不允许将一个void*类型指针赋予任何其他指针。用户在使用对象之前必须记住对它初始化。注意构造函数没有被调用,因为构造函数不能被显示的调用。在对象创建时由编译器调用。
4、operator new,分配内存并为这块内存调用构造函数。
MyType *fp=new MyType(1,2);一:调用malloc.二:使用参数表调用构造函数。this指针指向返回的对象的地址。
delete首先调用析构函数。然后释放内存free()。delete 表达式需要一个对象的地址。
如果正在删除的指针是0,则不发生任何事情。经常建议把指针在删除指针后立即把指针赋值为0,避免对它删除两次。
5、delete void指针,唯一发生的事情是释放了内存,但是没有调用析构函数。因此在删除它之前,先进行转换。
void *p=new string;
delete (string *)p;
6、用于数组的new和delete。
MyType *fp=new MyType[100];//在堆上为100个MyType对象分配了足够的内存并为每一个对象调用了构造函数。
fp是一个数组的起始地址。delete []fp;
7、耗尽内存:operator new 找不到足够大的连续内存来安排对象时,new-handler的特殊函数将会被调用。new-handler的默认动作是产生一个异常。<new>中定义new-handler。调用set_new_handler()函数。
#include <iostream>
#include <cstdlib>
#include <new>
using namespace std;
int count = 0;
void out_of_memory() {
cerr << "memory exhausted after " << count
<< " allocations!" << endl;
exit(1);
}
int main() {
set_new_handler(out_of_memory);
while(1) {
count++;
new int[1000]; // Exhausts memory
}
}
new_handler必须不带参数,且返回值为void。内存耗尽时,调用new_handler,new_handler试着调用operator new()。
8、重载的new与delete
new表达式:首先使用operator new 分配内存,然后调用构造函数。delete表达式里,首先调用析构函数,然后调用operator delete。
重载的new必须有size_t参数。它是奇偶uap分配内存的对象的长度。operator new的返回值是void *。做的是分配内存工作。没有完成对象的创建,直到构造函数被调用了才完成对象的创建。operator delete的参数是一个指向由operator new()分配的内存的void *。参数是一个void *。返回值类型void。
void* operator new(size_t sz) {
printf("operator new: %d Bytes\n", sz);
void* m = malloc(sz);
if(!m) puts("out of memory");
return m;
}
void operator delete(void* m) {
puts("operator delete");
free(m);
}
class S {
int i[100];
public:
S() { puts("S::S()"); }
~S() { puts("S::~S()"); }
};
int main() {
puts("creating & destroying an int");
int* p = new int(4);
delete p;
puts("creating & destroying an s");
S* s = new S;
delete s;
puts("creating & destroying S[3]");
S* sa = new S[3];
delete []sa;
}
//output
creating & destroying an int
operator new: 4 Bytes
operator delete
creating & destroying an s
operator new: 400 Bytes
S::S()
S::~S()
operator delete
creating & destroying S[3]
operator new: 1204 Bytes
S::S()
S::S()
S::S()
S::~S()
S::~S()
S::~S()
operator delete
operator delete
operator delete
9、对一个类重载new和delete。尽管不必显示的使用static,但实际上仍是在创建static成员函数。当编译器看到new创建自己定义的类的对象时,它选择成员版本的operator new()而不是全局版本的new().
为数组重载operator new[]和operator delete[].
ofstream trace("ArrayOperatorNew.out");
class Widget {
enum { sz = 10 };
int i[sz];
public:
Widget() { trace << "*"; }
~Widget() { trace << "hua~hua"; }
void* operator new(size_t sz) {
trace << "Widget::new: "
<< sz << " bytes" << endl;
return ::new char[sz];
}
void operator delete(void* p) {
trace << "Widget::delete" << endl;
::delete []p;
}
void* operator new[](size_t sz) {
trace << "Widget::new[]: "
<< sz << " bytes" << endl;
return ::new char[sz];
}
void operator delete[](void* p) {
trace << "Widget::delete[]" << endl;
::delete []p;
}
};
int main() {
trace << "new Widget" << endl;
Widget* w = new Widget;
trace << "\ndelete Widget" << endl;
delete w;
trace << "\nnew Widget[25]" << endl;
Widget* wa = new Widget[25];
trace << "\ndelete []Widget" << endl;
delete []wa;
}
//output
new Widget
Widget::new: 40 bytes
*
delete Widget
hua~huaWidget::delete
new Widget[25]
Widget::new[]: 1004 bytes
*************************
delete []Widget
hua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huahua~huaWidget::delete[]
当创建对象数组时,多了4字节,因为4字节是系统用来存放数组信息的。特别是数组中对象的数量。
第十四章
1、组合:在新类中创建已存在的类的对象。新类是由已存在的类的对象组合而成。
继承:创建一个新类作为一个已存在的类的类型,不修改已存在的类,而是采取这个已存在的类的形式,并将代码加入其中。
组合语法:
class Y{
X x;
};
Y y;
y.x.set();
继承语法:
class Y : public X {
int i; // Different from X's i
public:
Y() { i = 0; }
int change() {
i = permute(); // Different name call
return i;
}
void set(int ii) {
i = ii;
X::set(ii); // Same-name function call
}
};
int main() {
cout << "sizeof(X) = " << sizeof(X) << endl;
cout << "sizeof(Y) = "
<< sizeof(Y) << endl;
Y D;
D.change();
// X function interface comes through:
D.read();
D.permute();
// Redefined functions hide base versions:
D.set(12);
} //output:
sizeof(X) = 4
sizeof(Y) = 8
Y 对 X 进行了继承,Y将包含X中的所有数据成员和成员函数,实际上,正如没有对X进行继承,而在Y中创建了一个X的成员对象一样。Y是包含了X的一个子对象。无论是成员对象还是基类存储,都被认为是子对象。派生类可以重定义基类的函数,也可以增加新的函数。
2、构造函数的初始化表达式列表:在进入新类的构造函数体之前调用所有其他的构造函数。
class A {
int i;
public:
A(int ii) : i(ii) {}
~A() {}
void f() const {}
};
class B {
int i;
public:
B(int ii) : i(ii) {}
~B() {}
void f() const {}
};
class C : public B {
A a;
public:
C(int ii) : B(ii), a(ii) {}
~C() {} // Calls ~A() and ~B()
void f() const { // Redefinition
a.f();
B::f();
}
};
int main() {
C c(47);
}
只有通过继承,才能重定义它的函数。而对于成员对象,只能操作这个对象的公共接口而不能重定义它。如果C::f()还没有被定义,则对类型C的一个对象调用f()就不会调用a.f(),而会调用B::f();
3、构造函数与析构函数调用的顺序:
#include <fstream>
using namespace std;
ofstream out("order.out");
#define CLASS(ID) class ID { \
public: \
ID(int) { out << #ID " constructor\n"; } \
~ID() { out << #ID " destructor\n"; } \
};
CLASS(Base1);
CLASS(Member1);
CLASS(Member2);
CLASS(Member3);
CLASS(Member4);
class Derived1 : public Base1 {
Member1 m1;
Member2 m2;
public:
Derived1(int) : m2(1), m1(2), Base1(3) {
out << "Derived1 constructor\n";
}
~Derived1() {
out << "Derived1 destructor\n";
}
};
class Derived2 : public Derived1 {
Member3 m3;
Member4 m4;
public:
Derived2() : m3(1), Derived1(2), m4(3) {
out << "Derived2 constructor\n";
}
~Derived2() {
out << "Derived2 destructor\n";
}
};
int main() {
Derived2 d2;
}
//output:
Base1 constructor
Member1 constructor
Member2 constructor
Derived1 constructor
Member3 constructor
Member4 constructor
Derived2 constructor
Derived2 destructor
Member4 destructor
Member3 destructor
Derived1 destructor
Member2 destructor
Member1 destructor
Base1 destructor
构造函数的调用次序完全不受构造函数的初始化表达式表中的次序影响。该次序是由成员对象在类中声明的次序决定的。
4、名字隐藏:继承一个类,并对它的成员函数重新进行定义。分为两种情况:
1-正如基类中所进行的定义一样,在派生类的定义中明确地定义操作和返回类型,称之为对普通成员函数的重定义。
2-如果基类的成员函数是虚函数的情况,又可以称之为重写。
但是如果在派生类中改变了成员函数的参数列表和返回类型:
任何时候重新定义了基类的一个重载函数,在新类之中所有其他的版本则被自动隐藏了。
class Base {
public:
int f() const {
cout << "Base::f()\n";
return 1;
}
int f(string) const { return 1; }
void g() {}
};
class Derived1 : public Base {
public:
void g() const {}
};
class Derived2 : public Base {
public:
// Redefinition:
int f() const {
cout << "Derived2::f()\n";
return 2;
}
};
class Derived3 : public Base {
public:
// Change return type:
void f() const { cout << "Derived3::f()\n"; }
};
class Derived4 : public Base {
public:
// Change argument list:
int f(int) const {
cout << "Derived4::f()\n";
return 4;
}
};
int main() {
string s("hello");
Derived1 d1;
int x = d1.f();
d1.f(s);
Derived2 d2;
x = d2.f();
// d2.f(s); // string version hidden
Derived3 d3;
//! x = d3.f(); // return int version hidden
Derived4 d4;
//! x = d4.f(); // f() version hidden
x = d4.f(1);
}
//output
Base::f()
Derived2::f()
Derived4::f()
继承的目标是为了实现多态性。如果我们改变了函数特征或返回类型,实际上便改变了基类的接口。如果通过修改基类中一个成员函数的操作与或返回类型来改变了基类的接口,我们就没有使用继承通常所提供的功能,而是按另一种方式来重用该类。
5、非自动继承的函数:不是所有的函数都能自动地从基类继承到派生类中的。构造函数和析构函数用来处理对象的创建和析构操作。构造函数和析构函数不能被继承,必须为每一个特定的派生类分别创建。另外operator =也不能被继承。
继承和静态成员函数:静态成员函数与非静态成员函数的共同点:
1-他们均可以被继承到派生类中
2-如果我们重新定义了一个静态成员,所有在基类中的其他重载函数会被隐藏。
3-如果我们改变了基类中一个函数的特征,所有使用该函数名字的基类版本都将会被隐藏。
然而静态成员函数不可以是虚函数。
6、组合和继承都能把子对象放在新类型中,两个都使用构造函数的初始化表达式表去构造这些子对象。组合通常是在希望新类内部具有已存在类的功能时使用,而不是希望已存在类作为它的接口。就是说,嵌入一个对象,用以实现新类的功能,而新类的用户看到的是新定义的接口,而不是来自老类的接口,为此,在新类的内部嵌入已存在类的private对象。
7、私有继承:创建的新类具有基类的所有数据和功能,但这些功能是隐藏的,所以它只是部分的内部实现。该类的用户访问不到这些内部功能,并且一个对象不能被看做是这个基类的实例。派生类想产生像基类接口一样的接口部分,而不允许该对象的处理像一个基类对象,private继承提供了这个功能。
private继承:私有继承成员公有化:
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继承是有用的。
8、protected 成员:就这个类的用户而言,它是private的,但 它可被从这个类继承来的任何类使用。
9、除了赋值运算符,其他的运算符都可以被继承到派生类中。
10、向上类型转换
继承的最重要的方面不是它为新类提供了成员函数,而是它是基类与新类之间的关系。这种关系可以描述为:新类属于原有类的类型。
向上类型转换:从派生类到基类的类型转换。向上类型转换总是安全的。
如果允许编译器为派生类生成拷贝构造函数,它将首先自动地调用基类的拷贝构造函数,然后再是各成员对象的拷贝构造函数,因此可以得到正确的操作。
class Parent {
int i;
public:
Parent(int ii) : i(ii) {
cout << "Parent(int ii)\n";
}
Parent(const Parent& b) : i(b.i) {
cout << "Parent(const Parent&)\n";
}
Parent() : i(0) { cout << "Parent()\n"; }
friend ostream&
operator<<(ostream& os, const Parent& b) {
return os << "Parent: " << b.i << endl;
}
};
class Member {
int i;
public:
Member(int ii) : i(ii) {
cout << "Member(int ii)\n";
}
Member(const Member& m) : i(m.i) {
cout << "Member(const Member&)\n";
}
friend ostream&
operator<<(ostream& os, const Member& m) {
return os << "Member: " << m.i << endl;
}
};
class Child : public Parent {
int i;
Member m;
public:
Child(int ii) : Parent(ii), i(ii), m(ii) {
cout << "Child(int ii)\n";
}
//Child(const Child& c):i(c.i),m(c.m){cout<<"Child(const Child&)"<<endl;}
friend ostream&
operator<<(ostream& os, const Child& c){
return os << (Parent&)c << c.m
<< "Child: " << c.i << endl;
}
};
int main() {
Child c(2);
cout << "calling copy-constructor: " << endl;
Child c2 = c; // Calls copy-constructor
cout << "values in c2:\n" << c2;
}
//output:编译器为派生类自动生成拷贝构造函数。
Parent(int ii)
Member(int ii)
Child(int ii)
calling copy-constructor:
Parent(const Parent&)
Member(const Member&)
values in c2:
Parent: 2
Member: 2
Child: 2
//output:为派生类自定义拷贝构造函数。
Parent(int ii)
Member(int ii)
Child(int ii)
calling copy-constructor:
Parent()
Member(const Member&)
Child(const Child&)
values in c2:
Parent: 0
Member: 2
Child: 2
Child没有显示定义的拷贝构造函数,编译器将通过调研Parent 和 Member的拷贝构造函数来生成它的拷贝构造函数
为Child自己写拷贝构造函数时,基类部分调用默认的构造函数。这是在没有其他的构造函数可供选择调用的情况下,编译器回溯搜索的结果。
为了解决这个问题:必须记住,无论何时我们在创建了自己的拷贝构造函数时,都要正确地调用基类构造函数。
Child(const Child& c):Parent(c),i(c.i),m(c.m){cout<<"Child(const Child&)"<<endl;}
//output
Parent(int ii)
Member(int ii)
Child(int ii)
calling copy-constructor:
Parent(const Parent&)
Member(const Member&)
Child(const Child&)
values in c2:
Parent: 2
Member: 2
Child: 2
Parent(c)构造函数意味着:因为Child是由Parent继承而来,所以Child的引用也就相当于Parent的引用。基类拷贝构造函数的调用将一个Child的引用向上类型转换为一个Parent的引用,并且使用它来执行拷贝构造函数。
11、指针和引用的向上类型转换
Wind w;
Instrument *ip=&w;//upcast
Instrument &ir=w;//upcast
12、如果想重用已存在类型作为新类型的内部实现的话,我们最好用组合;
如果想使新的类型和基类的类型相同,则应使用继承。如果派生类有基类的接口,它就能向上类型转换到这个基类。
第十五章
1、多态性是面向对象程序设计语言中数据抽象和继承之外的第三个基本特征。
访问控制通过使细节数据设为private,将接口从具体实现中分离开来,多态性提供了接口与具体实现之间的另一层隔离。
2、虚函数增强了类型的概念,而不是只在结构内部隐蔽的封装代码。真正的OOP需要虚函数。
取一个对象的地址(指针或引用),并将其作为基类的地址来处理,称为向上类型转换。
3、虚函数:关键字virtual。仅仅在声明时需要使用关键字,在定义时不需要。如果一个函数在基类中被声明为virtual,那么在所有派生类,它都是virtual的。在派生类中virtual函数的重定义通常称为重写
注意:仅需要在基类中声明一个函数为virtual,调用所有匹配基类声明行为的派生类函数都将使用虚机制。
编译器对每个包含虚函数的类创建一个表。VTABLE。在表中放置特定类的虚函数的地址。在每个带有虚函数的类中,编译器秘密地放置一个指针,VPTR,指向这个对象的VTABLE。当通过虚类指针做虚函数调用时,编译器静态地插入能取得这个VPTR并在VTABLE中查找函数地址的代码。
设置VTABLE、初始化VPTR、为虚函数调用插入代码。
4、不带虚函数,对象的长度是所期望的长度,带有单个虚函数,对象的长度加void*的长度。对于每个VPTR,必须初始化为指向相应的VTABLE的起始地址。
class NoVirtual {
int a;
public:
void x() const {}
int i() const { return 1; }
};
class OneVirtual {
int a;
public:
virtual void x() const {}
int i() const { return 1; }
};
class TwoVirtuals {
int a;
public:
virtual void x() const {}
virtual int i() const { return 1; }
};
int main() {
cout << "int: " << sizeof(int) << endl;
cout << "NoVirtual: "
<< sizeof(NoVirtual) << endl;
cout << "void* : " << sizeof(void*) << endl;
cout << "OneVirtual: "
<< sizeof(OneVirtual) << endl;
cout << "TwoVirtuals: "
<< sizeof(TwoVirtuals) << endl;
}
//output
int: 4
NoVirtual: 4
void* : 4
OneVirtual: 8
TwoVirtuals: 8
5、如果想使用多态,就在每处使用虚函数。
在设计时,希望基类仅仅作为其派生类的一个接口。就是说,仅想对基类进行向上类型转换,使用它的接口,而不希望用户实际地创建一个基类的对象。可以在基类中加入纯虚函数,来使基类成为抽象类。virtual void play()const =0;编译器不允许生成抽象类的对象。
当继承一个抽象类时,必须实现所有的纯虚函数,否则继承出的类也将是一个抽象类。创建一个纯虚函数允许在接口中放置成员函数。抽象类为每个从他派生出来的类创建公共接口。
class Pet {
string pname;
public:
Pet(const string& petName) : pname(petName) {}
virtual string name() const { return pname; }
virtual string speak() const { return ""; }
};
class Dog : public Pet {
string name;
public:
Dog(const string& petName) : Pet(petName) {}
// New virtual function in the Dog class:
virtual string sit() const {
return Pet::name() + " sits";
}
string speak() const { // Override
return Pet::name() + " says 'Bark!'";
}
};
int main() {
Pet* p[] = {new Pet("generic"),new Dog("bob")};
cout << "p[0]->speak() = "
<< p[0]->speak() << endl;
cout << "p[1]->speak() = "
<< p[1]->speak() << endl;
//! cout << "p[1]->sit() = "
//! << p[1]->sit() << endl; // Illegal
}
6、使用多态的目的:让对基类对象操作的代码也能透明的操作派生类对象。
对象切片:对一个对象进行向上类型转换,不使用地址或引用。对象被切片。
7、不能在重新定义的过程中修改虚函数的返回类型。特例:如果返回一个指向基类的指针或引用,则该函数的重新定义版本将会从基类返回的内容中返回一个指向派生类的指针或引用。
8、构造函数是不能为虚函数的,但是析构函数能够且常常是必须是虚函数。这保证派生类对象被正确析构。
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;
}
//output
~Base1()
~Derived2()
~Base2()
9、纯虚析构函数
class AbstractBase {
public:
virtual ~AbstractBase() = 0;
};
AbstractBase::~AbstractBase() {}
class Derived : public AbstractBase {};
一般来说,如果在派生类中基类的纯虚函数没有重新定义,派生类将成为抽象类,然后,如果不进行析构函数定义,编译器将自动为每个类生成一个析构函数的定义。因此编译器会提供定义,并且派生类不会成为抽象类纯虚性的特性是阻止基类的实例化。
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
} ///:~
~Dog()
~Pet()
10、向下类型转换dynamic_cast
class Pet { public: virtual ~Pet(){}};
class Dog : public Pet {};
class Cat : public Pet {};
int main() {
Pet* b = new Cat; // Upcast
// Try to cast it to Dog*:
Dog* d1 = dynamic_cast<Dog*>(b);
// Try to cast it to Cat*:
Cat* d2 = dynamic_cast<Cat*>(b);
cout << "d1 = " << (long)d1 << endl;
cout << "d2 = " << (long)d2 << endl;
}
static_cast静态执行向下类型转换。
第十六章
1、继承和组合提供了重用对象代码的方法,C++模板特征提供了重用源代码的方法。
2、template <class T> 模板语法
template<class T>
class Array {
enum { size = 100 };
T A[size];
public:
T& operator[](int index) {
require(index >= 0 && index < size,
"Index out of range");
return A[index];
}
};
int main() {
Array<int> ia;
Array<float> fa;
for(int i = 0; i < 20; i++) {
ia[i] = i * i;
fa[i] = float(i) * 1.414;
}
for(int j = 0; j < 20; j++)
cout << j << ": " << ia[j]
<< ", " << fa[j] << endl;
}
Array<int> ia;是实例化模板。
非内联函数定义:
template<class T>
class Array {
enum { size = 100 };
T A[size];
public:
T& operator[](int index);
};
template<class T>
T& Array<T>::operator[](int index) {
require(index >= 0 && index < size,
"Index out of range");
return A[index];
}
int main() {
Array<float> fa;
fa[0] = 1.414;
}
3、模板中的常量
template<class T, int size = 100>
class Array {
T array[size];
public:
T& operator[](int index) {
require(index >= 0 && index < size,
"Index out of range");
return array[index];
}
int length() const { return size; }
};
class Number {
float f;
public:
Number(float ff = 0.0f) : f(ff) {}
Number& operator=(const Number& n) {
f = n.f;
return *this;
}
operator float() const { return f; }
friend ostream&
operator<<(ostream& os, const Number& x) {
return os << x.f;
}
};
template<class T, int size = 20>
class Holder {
Array<T, size>* np;
public:
Holder() : np(0) {}
T& operator[](int i) {
require(0 <= i && i < size);
if(!np) np = new Array<T, size>;
return np->operator[](i);
}
int length() const { return size; }
~Holder() { delete np; }
};
int main() {
Holder<Number> h;
for(int i = 0; i < 20; i++)
h[i] = i;
for(int j = 0; j < 20; j++)
cout << h[j] << endl;
}
//output
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19