c++全局变量无声明。多次定义会冲突。利用命名空间
c++四种cast操作符的区别
1.static_cast,支持子类指针到父类指针的转换,并根据实际情况调整指针的值,反过来也支持,但会给出编译警告,它作用最类似C风格的“强制转换”,一般来说可以认为它是安全的。
2.dynamic_cast,支持父类指针到子类指针的转换,并根据实际情况调整指针的值,和static_cast不同,反过来它就不支持了,会导致编译错误,这种转换是最安全的转换;
3.reinterpret_cast,支持任何转换,但仅仅是如它的名字所描述的那样“重解释”而已,不会对指针的值进行任何调整,用它完全可以做到指鹿为马,但很明显,它是最不安全的转换,使用它的时候,你得头脑清醒,知道自己在干什么。
4.const_cast,这个转换能剥离一个对象const或volatile属性,也就是说允许你对常量进行修改。
vs2013里定义头文件时,不能对cpp文件重命名为.h,属性不对,编译不过。
命名空间解决函数重名和变量重名。
迭代式(增量式)开发机制,命名空间提供。
1.可以分段定义;
2.空间变量和函数不能重名。
3.命名空间一般是公有的。
4.命名空间可以无限嵌套,加多层。
5.命名空间可以别名。
6.匿名命名空间可以编译,等价于全局变量,局部变量覆盖全局变量。
namespace
命名空间可以别名。
默认参数
1.默认参数不赋值时,必须位于最左侧,默认参数之间不允许有赋值的参数。
2.参数处理,从右向左处理
3.函数指针调用时,无法使用默认参数,需明确传参
4.函数重载冲突时,可以使用函数指针解决问题
const类型
1.c++是强类型检测,指针类型不匹配,编译不过。
2.c++编译器对const单个变量做了优化,直接读值,读常量符号表,不访问内存。
自动变量
auto num = 3/2.0;
sizeof(num)= ?;
根据右侧变量推导左侧存储空间大小
auto定义变量时,必须初始化
重载函数
1.参数个数有关
2.参数类型
3.参数顺序不同
4.与函数返回值无关
重载函数地址无法打印,需转换成函数指针后再打印。
引用
1.给一个已经存在的变量起一个别名,地址相同。
2.引用初始化的各种形式。
3.vs平台下int *p; int * (&rp)(p);g++平台下int *&rp(p).
4.引用定义时,需要初始化赋值
结构体内部的代码不占结构体存储空间。
new和malloc的区别?经测试,在统一存储空间分配内存。
4种类型转换:
1.const_cast
2.dynamic_cast
3.reinterpret_cast
4.static_cast
读书笔记c++ primer
第十二章
类的构造函数
1.如果类的成员变量是const或引用类型,使用初始化列表是不二选择。
类内部定义的函数默认为inline,在外部定义成员函数必须指明它们是在类的作用域中。
如:Sales_tem::avg_price的定义使用作用域。
const加在形参表之后,可以将成员函数声明为常量:
double avg_price() const;
const成员不能改变其所操作对象的数据成员。const必须同时出现在声明和定义中,若只出现在其中一处,就会出一个编译时错误。
类背后蕴涵的基本思想是数据抽象和封装。
数据抽象是一种依赖于接口和实现分离的编程技术。类的设计者必须关心类是如何实现的,但使用该类的程序员不必了解这些细节。相反使用一个类型的程序员仅需要了解类型的接口,他们可以抽象的考虑该类型做什么而不必考虑该类型如何工作。
如果类是用struct关键字定义的,则在第一个访问标号之前的成员是公有的,
如果类是用class关键字定义的,则这些成员是私有的。
具体类和抽象类型:并非所有类型都必须是抽象的。标准库中pair类就是一个实用的、设计良好的具体类而不是抽象类。具体类会暴露而非隐藏其实现细节。
好的类设计者会定义直观和易用的类接口,而使用者只关心类中影响他们使用的那部分实现。如果类的实现速度太慢或给类的使用者加上负担,则必然引起使用者的关注。在良好设计的类中,只有类的设计者会关心实现。
在简单的应用程序中,类的使用者和设计者也许是同一个人。即使在这种情况下,保持角色区分也是有益的,
设计类的接口时,设计者应该考虑的是如何方便类的使用;使用类的时候,设计者就不应该考虑类如何工作。
数据抽象和封装提供了两个重要优点:
1.避免类内部出现无意的、可能破坏对象状态的用户级错误。
2.随时间推移可以根据需求改变或缺陷(bug)报告来完善类实现,而无须改变用户级代码。
类型别名来简化类
class Screen {
public:
typedef std::string::size_type index;
private:
index cursor;
index height,width;
};
将index的定义放在类的public部分,是因为希望用户使用这个名字。Screen类的使用者不必了解用string实现的底层细节。
成员函数可被重载,类的成员函数与普通的非成员函数以及在其他类中声明的函数不相关,也不能重载它们。
两个重载成员函数的形参数量和类型不能完全相同。
例:
class Screen {
public:
typedef std::string::szie_type index;
char get() const {return contents[cursor];}
char get(index ht, index wd) const;
private:
std::string contents;
index cursor;
index height, width;
};
Screen myscreen;
char ch = myscreen.get();
ch = myscreen.get(0,0);
显式指定inline成员函数,在声明和定义处指定inline都是合法的。在类的外部定义inline的一个好处是可以使得类比较容易阅读。
inline成员函数的定义必须在调用该函数的每个源文件中是可见的。不在类定义体内定义的inline成员函数,其定义通常应放在有类定义的同一头文件中。
在一个给定的源文件中,一个类只能被定义一次。
如果在多个文件中定义一个类,那么每个文件中的定义必须是完全相同的。
将类定义放在头文件中,可以保证在每个使用类的文件中以同样的方式定义类。
声明一个类而不定义它:
class Screen;
这个声明有时称为前向声明,在声明之后,定义之前,类Screen是一个不完全类型(incompete type),即已知Screen是一个类型,但不知道包含哪些成员。
不完全类型只能以有限方式使用。
不能定义该类型的对象(不知成员,不知开辟多大存储)。
只能定义指向该类型的指针及引用,或者用于声明使用该类型作为形参类型或返回类型的函数。
在创建类的对象之前,必须完整的定义该类。
同样,在使用引用或指针访问类的成员之前,必须已经定义类。
只有当类定义已经在前面出现过,数据成员才能被指定为该类类型。如果该类型是不完全类型,那么数据成员只能是指向该类类型的指针或引用。
class LinkScreen{
Screen window;
LinkScreen *next;
LinkScreen *prev;
};
类的前向声明一般用来编写相互依赖的类。
定义对象风格:
Sales_item item1; //c++风格
class Sales_item item1; //c风格
成员函数不能定义this形参,而是由编译器隐含的定义。
成员函数的函数体可以显式的使用this指针,但不是必须这么做。如果对类成员的引用没有限定,编译器会将这种引用处理成通过this指针的引用。
何时使用this指针?尽管在成员函数内部显式引用this通常是不必要的,但有一种情况下必须这么做,当我们需要将一个对象作为整体引用而不是引用对象的一个成员时。最常见的情况是在这样的函数中使用this;该函数返回对调用该函数的对象的引用。
例:myScreen.move(4,0).set('#');
里面含有return (*this);
从const成员函数返回*this,在普通的非const成员函数中,this的类型是一个指向类类型的const指针。可以改变this所指向的值,但不能改变this所保存的地址。在const成员函数中,this的类型是一个指向const类类型对象的const指针。既不能改变this所指向的对象,也不能改变this所保存的地址。
不能从const成员函数返回指向类对象的普通引用。const成员函数只能返回*this作为一个const引用。
例:
display为const修饰成员函数,应用于长表达式中。
myScreen.display().set('*');
我们不能在一个const对象上调用set。
解决办法,基于const的重载。
class Screen {
public:
const Screen& display() const {};
Screen& display() {};
};
Screen myScreen(5,3);
const Screen blank(5,3);
myScreen.set('#').display(); //calls nonconst version
blank.display(); //cslls const version
可变数据成员,有时我们希望类的数据成员(甚至在const成员函数内)可以修改。
这可以通过将它们声明为mutable来实现。
可变数据成员永远都能为const,甚至当他是const对象的成员时也是如此。
形参表和函数体处于类作用域中,在定义于类外部的成员函数中,形参表和成员函数体都出现在成员名之后。这些都是在类作用域中定义,所以可以不用限定而引用其他成员。
例:
char Screen::get(index r, index c) const
{
index row = r* width;
return contents[row+c];
}
该函数用Screen内定义的index类型来指定其形参类型。
函数的返回类型不一定在类作用域中,与形参类型相比,返回类型出现在成员名字前面。
class Scree {
public:
typedef std::string::size_type index;
index get_cursor() const;
};
inline Screen::index Screen::get_cursor() const
{
return cursor;
}
编译器按照成员声明在类中出现的次序来处理它们,通常,名字必须在使用之前进行定义。而且,一旦一个名字被用作类型名,该名字就不能被重复定义。
类成员定义中的名字查找:
1.首先检查成员函数局部作用域中的声明。
2.其次检查对所有类成员的声明。
3.最后检查在此成员函数定义之前的作用域中出现的声明。
尽管全局对象被屏蔽了,但通过用全局作用域确定操作符来限定名字,仍然可以使用它
void dummy_fcn(index height)
{
cursor = width * ::height; //which height? the global one
}
构造函数的的名字于类的名字相同,并且不能指定返回类型。可以没有形参,也可以有多个形参
构造函数自动执行,只要创建该类型的一个对象,编译器就运行一个构造函数。
构造函数不能声明为const,创建类类型的const对象时,运行一个普通的构造函数来初始化该const对象。
例:
class Sales_item {
public:
Sales_item() const; //error
};
const Sales T;
与其他函数不同,构造函数可以包含一个初始化列表,构造函数可以定义在类的内部或外部。
构造函数初始化式只在构造函数的定义中而不是声明中指定。
不管成员是否在构造函数初始化列表中显式的初始化,类类型的数据成员总是在初始化阶段初始化。初始化发生在计算阶段开始之前。
使用构造函数初始化列表的版本初始化数据成员,发生在初始化阶段。函数体内对数据成员赋值的发生在函数计算执行阶段。
有些成员必须在构造函数初始化列表中进行初始化。对于这样的成员,在构造函数体中对他们赋值不起作用。
没有默认构造函数的类类型的成员,以及const或引用类型的成员,不管是哪种类型,都必须在构造函数初始化列表中进行初始化。
按照与成员声明一致的次序编写构造函数初始化列表是个好主意。此外,尽可能避免使用成员来初始化其他成员。
类通常应定义一个默认构造函数。假定有一个NoDefault类,定义了一个接受string实参的构造函数,则编译器不在合成默认构造函数。
1.必须通过传递一个初始化的string值给NoDefault的构造函数来显式的初始化NoDefault成员
2.编译器不再默认合成默认构造函数。
3.此类型不能用作动态分配数据的元素类型。
4.此类型的静态分配数组必须为每个元素提供一个显式的初始化式。
5.如果有一个保存NoDefault对象的容器,例如vector,就不能使用接受容器大小而没有同时提供一个元素初始化式的构造函数。
实际上,如果定义了其他构造函数,则提供一个默认构造函数几乎总是对的。
使用构造函数:
Sales_item myobj;
Sales_item myobj = Sales_item();
Sales_item myobj();错误
隐式类类型转换。可以用单个实参来调用的构造函数定义了从形参类型到该类类型的一个隐式转换。
class Sales_item {
public:
Sales_item(const std::string &book=""):
isbn(book), units_sold(0), revenue(0.0) {}
Sales_item(std::istream &is);
};
string null_book = "999";
item.same_isbn(null_book);
same_isbn本身期待一个Sales_item的参数,此时发生隐式类型转换,null_book调用构造函数,生成一个新的Sales_ttem对象(临时对象),被传递给了same_sibn。
一旦same_isbn结束,就不能再访问此临时对象,我们构造了一个在测试完成后被丢弃的对象,这个行为几乎肯定是一个错误。
抑制由构造函数定义的隐式转换,可以通过将构造函数声明为explicit,来防止在需要隐式转换的上下文中使用构造函数。
explicit关键字只能用于类内部的构造函数声明上。在类的定义体外部所做的定义上不再重复它。重复会报错。
class Sales_item {
public:
explicit Sales_item(const std::string &book=""):
isbn(book), units_sold(0), revenue(0.0) {}
explicit Sales_item(std::istream &is);
};
string null_book = "999";
item.same_isbn(null_book); //此时编译时会报错。
为转换而显式的使用构造函数
item.same_isbn(Sales_item(null_book));
通常,除非有明显的理由想要定义隐式转换,否则,单形参构造函数应该为explicit。
类成员的显式初始化,这是从c中继承而来的,有三个重大的缺点:
1.要求类的全体数据成员都是public。
2.将初始化每个对象的每个成员的负担放在程序员身上。乏味且易于出错的,因为容易遗忘初始化式或提供不适当的初始化式。
3.如果增加或删除一个成员,必须找到所有的初始化并正确更新。
友元,在某些情况下,允许特定的非成员函数访问一个类的私有成员,同时仍然阻止一般的访问,这是很方便做到的。
友元机制允许一个类将对其非公有成员的访问权授予指定的函数或类。友元的声明关键字friend。
它只能出现在类定义的内部,友元的声明可以出现在类中的任何地方。
通常,将友元声明成组的放在类定义的开始或结尾是个好主意。
友元类,友元函数。
定义友元类时,友元类必须先被定义过。
友元声明将已命名的类或非成员函数引入到外围作用域中。此外,友元函数可以在类的内部定义,该函数的作用域扩展到包围该类定义的作用域。
static数据成员独立于该类的任意对象而存在,是与类关联的对象。
static成员函数没有this形参,它可以直接访问所属类的static成员,但不能访问非static成员。
使用类的static成员的优点:
1.static成员的名字在类的作用域中,因此可以避免与其他类的成员或全局对象名字冲突。
2.可以实施封装,static成员可以是私有成员,而全局变量不可以。
3.通过阅读程序容易看出static成员是与特定类关联的。
在类的外部定义static成员时,无须重复指定static保留字,该保留字只出现在类定义体内部的声明处。
static成员函数没有this指针,因为它不属于任何对象的组成部分。
static关键字只能用于类定义体内部的声明中,定义不能标示为static。
double Account::interestRate = initRate();
其中interestRate在类中是static声明修饰,在类外定义时不能再加static,initRate是类中的私有函数,前面已经有类作用域的声明,后面可以直接使用类中的initRate函数。
static在类中声明变量,在类外定义变量,存储空间开辟在数据段上。
例外,const static修饰的变量可以在类体中直接赋值。但也需要在类外定义此变量的存储空间。定义时不需要再赋值。
static成员函数不与对象绑定,没有this指针
第十三章
拷贝(复制)构造函数:是一种特殊的构造函数,具有单个形参,该形参(常用const修饰)是对该类类型的引用。
当定义一个新对象并用一个同类型的对象对它进行初始化时,将显式使用拷贝构造函数。当将该类型的对象传递给函数或从函数返回该类型对象时,将隐式使用拷贝构造函数。
析构函数是构造函数的互补:当对象超出作用域或动态分配的对象被删除时,将自动应用析构函数。析构函数可用于释放对象所获取的资源。
不管类是否定义了自己的析构函数,编译器都自动执行类中非static数据成员的析构函数。
与默认构造函数一样,拷贝构造函数可以由编译器隐式调用。拷贝构造函数可用于:
1.根据另一个同类型的对象显式或隐式初始化一个对象。
2.复制一个对象,将它作为实参传给一个函数。
3.从函数返回时复制一个对象。
4.初始化顺序容器中的元素。
5.根据元素初始化式列表初始化数组元素。
默认合成的拷贝构造函数,执行逐个成员初始化,将新对象初始化为原对象的副本。
数组是个例外,虽然一般不能直接复制数组,但如果一个类具有数组成员,则合成拷贝构造函数将复制整个数组。
因为用于向函数传递对象和从函数返回对象,该构造函数一般不应设置为explicit。
通常,定义拷贝构造函数最困难的部分在于认识到需要拷贝构造函数。只有能认识到需要拷贝构造函数,定义拷贝构造函数一般非常简单。
对象创建时用另一个对象初始化赋值,调用拷贝构造函数。
对象已经存在,赋值时,调用赋值函数。
如果我们不想实在不想编写拷贝构造函数和赋值函数,又不允许别人使用编译器默认提供的缺省函数怎么办?
只需要将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。因为编译器在默认调用这类函数时,发现其为私有成员函数,则编译报错。
为了防止复制,类必须显式声明其拷贝构造函数为private。
如果拷贝构造函数是私有的,将不允许用户代码复制该类类型的对象,编译器将拒绝任何进行复制的尝试。
然而,友元和成员函数可以进行复制。如果想连友元和成员函数中的复制也禁止,就可以声明一个private拷贝构造函数但不对其定义。
声明而不定义成员函数是合法的,但是,使用未定义成员的任何尝试将导致链接失败。
通过声明(但不定义)private拷贝构造函数,可以禁止任何复制类类型对象的尝试:用户代码中的复制尝试将在编译时标记为错误,而成员函数和友元中的复制尝试将在连接时导致错误。
一般来说,最好显式或隐式定义默认构造函数和拷贝构造函数。只有不存在其它构造函数时才合成默认构造函数。如果定义了拷贝构造函数,也必须定义默认构造函数。
大多数操作符可以定义为成员函数或非成员函数。当操作符为成员函数时,它的第一个操作数隐式绑定到this指针。因此,赋值操作符接受单个形参,且该形参是同一类类型的对象。右操作数一般为const引用传递。
例如:
Sales_item& Sales_item::operator=(const Sales_item &rhs)
{
成员逐个赋值。。。
* 删除已开辟空间。。。
申请新空间。。。
拷贝。。。
return *this;
}
一般而言,如果类需要拷贝构造函数,我们几乎肯定也需要赋值操作符。
构造函数的一个用途是自动获取资源。例如,构造函数可以分配一个缓冲区或打开一个文件,在构造函数中分配了资源之后,需要一个对应操作自动回收或释放资源。析构函数就是这样的一个特殊函数,它可以完成所需的资源回收,作为类构造函数的补充。
容器中的元素是按逆序撤销。
如果类需要析构函数,则它也需要赋值操作符合拷贝构造函数,这是一个有用的经验法则。这个规则常称为三法则(rule of three),指的是如果需要析构函数,则需要所有这三个复制控制成员。
合成析构函数按对象创建时的逆序撤销每个非static成员。
析构函数是个成员函数,它的名字是在类名字之前加上一个~,它没有返回值,没有形参。因为不能指定任何形参,所以不能重载析构函数。
析构函数与拷贝构造函数或赋值操作符之间的一个重要区别是,即使我们编写了自己的析构函数,合成析构函数仍然运行。先执行自己定义的析构函数,再执行合成析构函数。
理解:自己的析构函数销是合成析构函数的一个扩展补充,负责毁堆资源和关闭文件,合成析构函数负责销毁普通的数据成员。
管理指针成员:
1.指针成员采取常规指针行为。这样的类具有指针的所有缺陷但无需特殊的复制控制。
2.类可以实现所谓的“智能指针”行为。指针所指向的对象时共享的,但类能够防止悬垂指针。
3.类采取值型行为。指针所指向的对象时唯一的,由每个类对象独立管理。
智能指针?
悬垂指针:指针指向的存储空间已被释放。
第十四章 重载操作符与转换
重载操作符是具有特殊名称的函数:保留字operator后接需定义的操作符符号。像任意其他函数一样,重载操作符具有返回类型和形参表,如下语句:
Sales_item ooperator+(const Sales_itme& , const Sales_item&);
不可重载的操作符
:: .* . ?:
其它的操作符都可以重载。
重载操作符必须具有至少一个类类型或枚举类型的操作数。这条规则强制重载操作符不能重新定义用于内置类型对象的操作符的含义。
优先级和结核性是固定的。
不再具备短路求值特性。
类成员与非成员,大多数重载操作符可以定义为普通非成员函数或类的成员函数。
作为类成员的重载函数,其形参看起来比操作数数目少1.作为成员函数的操作符有一个隐含的this形参,限定为第一个操作数。
一般将算数和关系操作符定义为非成员函数,而将赋值操作定义为成员。
重载操作符的设计。
1.不要重载具有内置含义的操作符。重载逗号,取地址,逻辑与、逻辑或等操作符通常不是好做法。这些操作符具有有用的内置含义,如果我们定义了自己的版本,就不能再使用这些内置含义。
2.大多数操作符对类对象没有意义。一般相等测试应使用operator==;输入输出使用移位操作符;测试对象是否为空使用operator!。
3.如果一个类有算数操作符或位操作符,那么提供相应的复合赋值操作符一般是个好做法。
4.相等和关系操作符,关联容器应该定义== 、<操作符,许多算法假定这些操作符存在。如果定义了==,则应定义!=,如果定义了<,则应定义(>,>=,<,<=)。
5.选择成员或非成员实现。
原则:
=、下标[]、调用()和成员访问->等操作符必须定义为成员函数,否则编译报错。
复合赋值操作符通常定义为类的成员。与赋值不同的是,不一定非得这样做,编译不报错。
改变对象状态或与给定类型紧密联系的其它一些操作符,如自增自减和解引用,通常定义为成员函数。
对称的操作符,如算数操作符、相等操作符、关系操作符和位操作符,最好定义为普通非成员函数。
当一个重载操作符的定义不明显时,给操作取一个名字更好。对于很少用的操作,使用命名函数通常也比用操作符更好。如果不是普通操作,没有必要为简洁而使用操作符。
输入输出IO操作必须为非成员函数。否则,左操作数只能是该类类型的对象:
Sales_item Item;
item << cout;
这个用法与为其他类型定义的输出操作符的正常使用方式相反。
输出操作符通常所做格式化应尽量少,并且不应该输出换行符。
输入操作符>>的重载,必须处理错误和文件结束的可能性。
设计输入操作时,如果可能,要确定错误恢复措施,这很重要。
如果既定义了算数操作符又定义了相关复合赋值操作符的类,一般应用复合赋值实现算数操作符。如+,用+=去实现,比其它方式更简单且更有效,不必创建和销毁一个临时来保存+的结果。
赋值必须返回对*this的引用。
类定义下标操作符时,一般需要定义两个版本:一个为非const成员并返回引用,另一个为const成员并返回const引用。
为了支持指针类型,例如迭代器,c++语言允许重载解引用操作符*和箭头->。
->必须定义为类成员函数。*不要求定义为成员,但将它作为成员一般也是正确的。
区别前缀++和后缀++,后缀式操作符函数接受一个额外的(无用的)int型形参。前缀不需要参数。
调用函数()重载???
第十五章 面向对象编程
面向对象编程基于三个基本概念:数据抽象、继承和动态绑定。在c++中用类进行数据抽象,用类派生从一个类继承另一个类:派生类继承基类的成员。动态绑定使编译器能够在运行时决定使用基类中定义的函数还是派生类中定义的函数。
多态性紧用于通过继承而相关的类型的引用或指针。
在c++中,基类必须指出希望派生类重定义哪些函数,定义为virtual的函数是基类期待派生类重新定义的,基类希望派生类继承的函数不能定义为虚函数。
动态绑定,我们能够编写程序使用继承层次中任意类型的对象,无须关心对象的具体类型。使用这些类的程序无须区分函数是在基类还是在派生类中定义的。
通过基类的引用(或指针)调用虚函数时,发生动态绑定。引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指对象的实际类型所定义的。
protected成员不能被类的用户访问,可以被该类的派生类访问。
派生类只能通过派生类对象访问其基类的protected成员,派生类对其基类类型对象的protected成员没有特殊访问权限。
为了定义派生类,使用类派生列表指定基类。派生列表指定了一个或多个基类,具有如下形式:
class classname: access-lable base-class
这里access-lable是public、protected、private,base-class是已定义的类的名字。类派生列表可以指定多个基类。继承单个基类最为常见。
尽管不是必须这样做,派生类一般会重定义所继承的虚函数。如果派生类没有重定义某个虚函数,则使用基类中定义的版本。
派生类型必须对想要重定义的每个继承成员进行声明。
派生类中虚函数的声明必须与基类中的定义方式完全匹配,但有一个例外:返回对基类型的引用(或指针)的虚函数。派生类中的虚函数可以返回基类函数所返回类型的派生类的引用(或指针)。
如:Item_base类可以定义返回Item_base*的虚函数,如果这样,派生类Bulk_item类中定义的实例可以定义返回Item_base*或Bulk_item*。
一旦函数在基类中声明为虚函数,它就一直未虚函数,派生类无法改变该函数为虚函数这一事实。派生类重定义虚函数时,可以使用virtual保留字,但不是必须这样做。
c++语言不要求编译器将对象的基类部分和派生类部分连续排列。
派生类中的函数可以使用基类的成员。
用作基类的类必须是已定义的。
从效果来说,最底层的派生类对象包含其每个直接基类和间接基类的子对象。
c++中的函数调用默认不使用动态绑定。要触发动态绑定,必须满足两个条件:
1.只有指定为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数,非虚函数不进行动态绑定;
2.必须通过基类类型的引用或指针进行函数调用。
引用和指针的静态类型与动态类型可以不同,这是c++用以支持多态性的基石。
通过基类引用或指针调用基类中定义的函数时,我们并不知道执行函数的对象的确切类型,执行函数的对象可能是基类型的,也可能是派生类型的。
如果调用非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数。
如果调用虚函数,则直到运行时才能确定调用哪个函数,运行的虚函数是引用所绑定的或是指针所指向的对象所属类型定义的版本。
在编译时确定非virtual调用。
覆盖虚函数机制。在某些情况下,希望覆盖虚函数机制并强制函数调用使用虚函数的特定版本,这时可以使用域操作符:
例:
Item_base *basep = &derived; //derived是派生类的实例对象
double d = basep->Item_base::net_price(43);
只有成员函数中的代码才应该使用作用域操作符覆盖虚函数机制。
派生类虚函数调用基类版本时,必须显式使用作用域操作符。如果派生类函数忽略了这样做,则函数调用会在运行时确定并且将是一个自身调用,从而导致无穷递归。
虚函数与默认实参,虚函数也可以有默认实参。通常,如果有用在给定调用中的默认实参值,该值将在编译时确定。如果一个调用省略了具有默认值的实参,则所用的值由调用函数的类型定义,与对象的动态类型无关。通过基类的引用或指针调用虚函数时,默认实参为基类虚函数声明中指定的值,如果通过派生类的指针或引用调用虚函数,则默认实参是派生类的版本中声明的值。
在同一虚函数的基类版本和派生类版本中使用不同的默认实参几乎一定会引起麻烦。
如果通过基类的引用或指针调用虚函数,但实际执行的是派生类中定义的版本,这时可能会出现问题。
公用、私有和受保护的继承。
每个类控制它所定义的成员的访问。派生类可以进一步限制但不能放松对所继承的成员的访问。
如果是公有继承public,基类成员保持自己的访问级别。
如果是受保护继承protected,基类的public和protected成员在派生类中为protected成员。
如果是私有继承private,基类的所有成员在派生类中为private成员。
无论派生列表中是什么访问标号,所有派生类内部对基类成员具有相同的访问。派生类访问标号将控制派生类用户对从基类继承而来的成员的访问。
使用private或protected派生的类不继承基类的接口,相反,这些派生通常称为实现继承。派生类在实现中使用被继承类但继承基类的部分并未成为其接口的一部分。
迄今为止,最常见的继承形式是public。
派生类可以恢复继承成员的访问级别,但不能使访问级别比基类中原来指定的更严格或更宽松。使用using声明访问基类中的名字。
友元关系不能继承。基类的友元对派生类的成员没有特殊访问权限。如果基类被授予友元关系,则只有基类具有特殊访问权限,该基类的派生类不能访问授予友元关系的类。
如果基类定义了static成员,则整个继承层次中只有一个这样的成员。
每个派生类对象包含一个基类部分,所以存在从派生类型引用到基类类型引用的自动转换。
没有从基类引用(或指针)到派生类引用(或指针)的(自动)转换。
派生类构造函数,受继承关系影响,每个派生类构造函数除了初始化自己的数据成员之外,还要初始化基类。
构造函数初始化列表为类的基类和成员提供初始值,它并不指定初始化的执行次序。首先初始化基类,然后根据声明次序初始化派生类的成员。
只能初始化直接基类。直接基类就是在派生列表中指定的类。
重构包括重新定义类层次。为了适应应用程序的需要而重新设计类以便增加新函数或处理其他改变时,最有可能需要进行重构,重构常见在面向对象应用程序中非常常见。
如果析构函数为虚函数,那么通过指针调用时,运行哪个析构函数将因指针所指对象类型的不同而不同。
即使析构函数没有工作要做,继承层次的根类也应该定义一个虚析构函数。
构造函数不是虚函数。构造函数是在对象完全构造之前运行的,在析构函数运行的时候,对象的动态类型还不完整。
将类的赋值操作符设为虚函数很可能令人混淆,而且不会有什么用处。
与基类成员同名的派生类成员将屏蔽对基类成员的直接访问。使用作用域操作符访问被屏蔽的成员。
在派生类中使用同一名字的成员函数,其行为与数据成员一样,即使函数原型不同,基类成员也会被屏蔽。
如果派生类重定义了重载成员,则通过派生类型只能访问派生类中重定义的那些成员。
如果派生类想通过自身类型使用所有的重载版本,则派生类必须要么重定义所有重载版本,要么一个也不重定义。
否则使用using声明???
纯虚函数,在函数形参表后面写上=0.
含有(或继承)一个或多个纯虚函数的类是抽象类。除了作为抽象基类的派生类的对象的组成部分,不能创建抽象类的对象。
第十六章 模板与泛型编程
面向对象编程中的多态性在运行时应用于存在继承关系的类。我们能够编写使用这些类的代码,忽略基类与派生类之间类型上的差异。
只要使用基类的引用或指针,基类类型或派生类类型的对象就可以使用相同的代码。
运行时多态性,编译时多态性。
在泛型编程中,我们所编写的类和函数能够多态的用于跨越编译时不相关的类型。一个类或一个函数可以用来操作多种类型的对象。
c++中模板是泛型编程的基础。
模板定义以关键字template开始,后接模板形参表,模板形参表是用尖括号括住的一个或多个模板形参的列表,形参之间以逗号分隔。
模板形参表不能为空。
template <typename T>
int compare(const T &v1, const T &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
模板形参可以是表示类型的类型形参,也可以是表示常量表达式的非类型形参。
使用函数模板时,编译器会推断哪个模板实参绑定到模板形参。
一旦编译器确定了实际的模板实参,就称它实例化了函数模板的一个实例。
推导出实际模板实参后,编译器使用实参代替相应的模板形参产生并编译该版本的函数。
编译器承担了为我们使用每种类型而编写函数的单调工作。
inline函数模板,需放在模板形参表之后,返回类型之前,不能放在template之前。
每个模板类型形参前必须带上关键字class或typename,每个非类型形参前面必须带上类型名字,省略关键字或类型说明符是错误的。
例如:
template <typename T, U> T calc(const T&, const U&);
类型形参由关键字class或typename后接说明符构成。这两个关键字具有相同的含义,都指出后面所接的名字表示一个类型。
typename代替关键字class更直观,毕竟,可以使用内置类型作为实际的类型形参。
模板类型形参可以用于指定返回类型或函数形参类型,以及在函数体中用于变量声明或强制类型转换。
通过在成员名前加上关键字typename作为前缀,可以告诉编译器将成员当做类型。
例:
template <class Parm, class U>
Parm fcn(Parm* array, U value)
{
typename Parm::size_type *p; //类型or成员?
}
非类型模板形参。在调用非类型形参将用值代替,值的类型在模板形参表中指定。
例:
template <class T, size_t N> void array_init(T (&parm)[N])
{
for (size_t i = 0; i != N; ++i)
parm[i] = 0;
}
通过将形参设为const引用,就可以使用不允许复制的类型。大多数类型(包括内置类型,除IO类型之外)都允许复制。但是也有不允许复制的类类型。将形参设为const引用,保证这种类型可以用于compare函数,而且,如果有比较大的对象调用compare,则这个设计还可以运行的更快。
模板是一个蓝图,它本身不是类或函数。编译器用模板产生指定的类或函数的特定类型版本。产生模板的特定类型实例的过程称为实例化。
类模板的每次实例化都会产生一个独立的类类型。与其他任意实例类型没有关系。
类模板的形参是必须的。
Queue qs; 错误
Queue<int> qi;
Queue<string> qs;
Queue不是类型
使用函数模板时,编译器通常会为我们推断模板实参:
int main()
{
compare(1,0);
compare(3.14, 2.7);
return 0;
}
多个类型形参的实参必须完全匹配。如果推断类型不匹配,则调用会出错。
一般而言,不会转换实参以匹配已有的实例化,相反,会产生新的实例。除了产生实例之外,编译器只会执行两种转换:
1.const转换,接受const引用或const指针的函数可以分别用非const对象的引用或指针来调用,无须产生新的实例化。
2.数组或函数到指针的转换,如果模板形参不是引用类型,则对数组或函数类型的实参应用常规指针转换。
模板实参推断与函数指针,可以使用函数模板对函数指针进行初始化或赋值,编译器使用指针的类型实例化具有适当模板实参的模板版本。
例如:
template <typename T> int compare(const &T, const &T);
int (*pf1)(const int &, const int &) = compare;
Queue类实现
template <class Type> class Queue {
public:
Queue() : head(0), tail(0) { }
Queue(const Queue &Q) : head(0),tail(0) {copy_elems(Q);}
Queue& operator=(const Queue&);
~Queue() {destroy();}
Type &front() {return head->item;}
const Type &front() const {return head->item;}
void push(const Type &);
void pop();
bool empty() const { return head == 0; }
private:
QueueItem<Type> *head;
QueueItem<Type> *tail;
void destroy();
void copy_elems(const Queue&);
};
通常,当使用类模板的名字时候,必须指定模板形参。这一规则有个例外,在类本身的作用域内部,可以使用类模板的非限定名。
如Queue是Queue<Type>的缩写。
类模板成员函数的定义具有如下形式:
1.必须以关键字template开头,后接类的模板形参表。
2.必须指出它是哪个类的成员。
3.类名必须包含其模板形参。
如:template <class T> ret-type Queue<T>::member-name
如:
template <class Tyte> void Queue<Type>::destroy()
{
while (!empty())
pop();
}
非类型模板实参必须是编译时常量表达式。
template <int hi, int wid> class Screen {};
Screen<24, 80>hp2621;
hi的模板实参是24,wid的模板实参是80.
类模板中的友元声明:
1.普通非模板类或函数的友元声明,每一种都声明了一个或多个实体的友元关系。
2.类模板或函数模板的友元声明,授予对友元所有实例的访问权。
3.只授予对类模板或函数模板的特定实例的访问权的友元声明。
模板特化
函数模板特化:
1.关键字template后面接一对空的尖括号<>;
2.再接模板名和一对尖括号,尖括号中指定这个特化定义的模板形参;
3.函数形参表。
4.函数体。
处理C风格的字符串,如:
template <> int compare<const char *>(const char * const &v1, const char* const &v2)
{
return strcmp(v1,v2);
}
c++四种cast操作符的区别
1.static_cast,支持子类指针到父类指针的转换,并根据实际情况调整指针的值,反过来也支持,但会给出编译警告,它作用最类似C风格的“强制转换”,一般来说可以认为它是安全的。
2.dynamic_cast,支持父类指针到子类指针的转换,并根据实际情况调整指针的值,和static_cast不同,反过来它就不支持了,会导致编译错误,这种转换是最安全的转换;
3.reinterpret_cast,支持任何转换,但仅仅是如它的名字所描述的那样“重解释”而已,不会对指针的值进行任何调整,用它完全可以做到指鹿为马,但很明显,它是最不安全的转换,使用它的时候,你得头脑清醒,知道自己在干什么。
4.const_cast,这个转换能剥离一个对象const或volatile属性,也就是说允许你对常量进行修改。
vs2013里定义头文件时,不能对cpp文件重命名为.h,属性不对,编译不过。
命名空间解决函数重名和变量重名。
迭代式(增量式)开发机制,命名空间提供。
1.可以分段定义;
2.空间变量和函数不能重名。
3.命名空间一般是公有的。
4.命名空间可以无限嵌套,加多层。
5.命名空间可以别名。
6.匿名命名空间可以编译,等价于全局变量,局部变量覆盖全局变量。
namespace
命名空间可以别名。
默认参数
1.默认参数不赋值时,必须位于最左侧,默认参数之间不允许有赋值的参数。
2.参数处理,从右向左处理
3.函数指针调用时,无法使用默认参数,需明确传参
4.函数重载冲突时,可以使用函数指针解决问题
const类型
1.c++是强类型检测,指针类型不匹配,编译不过。
2.c++编译器对const单个变量做了优化,直接读值,读常量符号表,不访问内存。
3.const数组,需要读内存
自动变量
auto num = 3/2.0;
sizeof(num)= ?;
根据右侧变量推导左侧存储空间大小
auto定义变量时,必须初始化
重载函数
1.参数个数有关
2.参数类型
3.参数顺序不同
4.与函数返回值无关
重载函数地址无法打印,需转换成函数指针后再打印。
引用
1.给一个已经存在的变量起一个别名,地址相同。
2.引用初始化的各种形式。
3.vs平台下int *p; int * (&rp)(p);g++平台下int *&rp(p).
4.引用定义时,需要初始化赋值
结构体内部的代码不占结构体存储空间。
new和malloc的区别?经测试,在统一存储空间分配内存。
4种类型转换:
1.const_cast
2.dynamic_cast
3.reinterpret_cast
4.static_cast
读书笔记c++ primer
第十二章
类的构造函数
1.如果类的成员变量是const或引用类型,使用初始化列表是不二选择。
类内部定义的函数默认为inline,在外部定义成员函数必须指明它们是在类的作用域中。
如:Sales_tem::avg_price的定义使用作用域。
const加在形参表之后,可以将成员函数声明为常量:
double avg_price() const;
const成员不能改变其所操作对象的数据成员。const必须同时出现在声明和定义中,若只出现在其中一处,就会出一个编译时错误。
类背后蕴涵的基本思想是数据抽象和封装。
数据抽象是一种依赖于接口和实现分离的编程技术。类的设计者必须关心类是如何实现的,但使用该类的程序员不必了解这些细节。相反使用一个类型的程序员仅需要了解类型的接口,他们可以抽象的考虑该类型做什么而不必考虑该类型如何工作。
如果类是用struct关键字定义的,则在第一个访问标号之前的成员是公有的,
如果类是用class关键字定义的,则这些成员是私有的。
具体类和抽象类型:并非所有类型都必须是抽象的。标准库中pair类就是一个实用的、设计良好的具体类而不是抽象类。具体类会暴露而非隐藏其实现细节。
好的类设计者会定义直观和易用的类接口,而使用者只关心类中影响他们使用的那部分实现。如果类的实现速度太慢或给类的使用者加上负担,则必然引起使用者的关注。在良好设计的类中,只有类的设计者会关心实现。
在简单的应用程序中,类的使用者和设计者也许是同一个人。即使在这种情况下,保持角色区分也是有益的,
设计类的接口时,设计者应该考虑的是如何方便类的使用;使用类的时候,设计者就不应该考虑类如何工作。
数据抽象和封装提供了两个重要优点:
1.避免类内部出现无意的、可能破坏对象状态的用户级错误。
2.随时间推移可以根据需求改变或缺陷(bug)报告来完善类实现,而无须改变用户级代码。
类型别名来简化类
class Screen {
public:
typedef std::string::size_type index;
private:
index cursor;
index height,width;
};
将index的定义放在类的public部分,是因为希望用户使用这个名字。Screen类的使用者不必了解用string实现的底层细节。
成员函数可被重载,类的成员函数与普通的非成员函数以及在其他类中声明的函数不相关,也不能重载它们。
两个重载成员函数的形参数量和类型不能完全相同。
例:
class Screen {
public:
typedef std::string::szie_type index;
char get() const {return contents[cursor];}
char get(index ht, index wd) const;
private:
std::string contents;
index cursor;
index height, width;
};
Screen myscreen;
char ch = myscreen.get();
ch = myscreen.get(0,0);
显式指定inline成员函数,在声明和定义处指定inline都是合法的。在类的外部定义inline的一个好处是可以使得类比较容易阅读。
inline成员函数的定义必须在调用该函数的每个源文件中是可见的。不在类定义体内定义的inline成员函数,其定义通常应放在有类定义的同一头文件中。
在一个给定的源文件中,一个类只能被定义一次。
如果在多个文件中定义一个类,那么每个文件中的定义必须是完全相同的。
将类定义放在头文件中,可以保证在每个使用类的文件中以同样的方式定义类。
声明一个类而不定义它:
class Screen;
这个声明有时称为前向声明,在声明之后,定义之前,类Screen是一个不完全类型(incompete type),即已知Screen是一个类型,但不知道包含哪些成员。
不完全类型只能以有限方式使用。
不能定义该类型的对象(不知成员,不知开辟多大存储)。
只能定义指向该类型的指针及引用,或者用于声明使用该类型作为形参类型或返回类型的函数。
在创建类的对象之前,必须完整的定义该类。
同样,在使用引用或指针访问类的成员之前,必须已经定义类。
只有当类定义已经在前面出现过,数据成员才能被指定为该类类型。如果该类型是不完全类型,那么数据成员只能是指向该类类型的指针或引用。
class LinkScreen{
Screen window;
LinkScreen *next;
LinkScreen *prev;
};
类的前向声明一般用来编写相互依赖的类。
定义对象风格:
Sales_item item1; //c++风格
class Sales_item item1; //c风格
成员函数不能定义this形参,而是由编译器隐含的定义。
成员函数的函数体可以显式的使用this指针,但不是必须这么做。如果对类成员的引用没有限定,编译器会将这种引用处理成通过this指针的引用。
何时使用this指针?尽管在成员函数内部显式引用this通常是不必要的,但有一种情况下必须这么做,当我们需要将一个对象作为整体引用而不是引用对象的一个成员时。最常见的情况是在这样的函数中使用this;该函数返回对调用该函数的对象的引用。
例:myScreen.move(4,0).set('#');
里面含有return (*this);
从const成员函数返回*this,在普通的非const成员函数中,this的类型是一个指向类类型的const指针。可以改变this所指向的值,但不能改变this所保存的地址。在const成员函数中,this的类型是一个指向const类类型对象的const指针。既不能改变this所指向的对象,也不能改变this所保存的地址。
不能从const成员函数返回指向类对象的普通引用。const成员函数只能返回*this作为一个const引用。
例:
display为const修饰成员函数,应用于长表达式中。
myScreen.display().set('*');
我们不能在一个const对象上调用set。
解决办法,基于const的重载。
class Screen {
public:
const Screen& display() const {};
Screen& display() {};
};
Screen myScreen(5,3);
const Screen blank(5,3);
myScreen.set('#').display(); //calls nonconst version
blank.display(); //cslls const version
可变数据成员,有时我们希望类的数据成员(甚至在const成员函数内)可以修改。
这可以通过将它们声明为mutable来实现。
可变数据成员永远都能为const,甚至当他是const对象的成员时也是如此。
形参表和函数体处于类作用域中,在定义于类外部的成员函数中,形参表和成员函数体都出现在成员名之后。这些都是在类作用域中定义,所以可以不用限定而引用其他成员。
例:
char Screen::get(index r, index c) const
{
index row = r* width;
return contents[row+c];
}
该函数用Screen内定义的index类型来指定其形参类型。
函数的返回类型不一定在类作用域中,与形参类型相比,返回类型出现在成员名字前面。
class Scree {
public:
typedef std::string::size_type index;
index get_cursor() const;
};
inline Screen::index Screen::get_cursor() const
{
return cursor;
}
编译器按照成员声明在类中出现的次序来处理它们,通常,名字必须在使用之前进行定义。而且,一旦一个名字被用作类型名,该名字就不能被重复定义。
类成员定义中的名字查找:
1.首先检查成员函数局部作用域中的声明。
2.其次检查对所有类成员的声明。
3.最后检查在此成员函数定义之前的作用域中出现的声明。
尽管全局对象被屏蔽了,但通过用全局作用域确定操作符来限定名字,仍然可以使用它
void dummy_fcn(index height)
{
cursor = width * ::height; //which height? the global one
}
构造函数的的名字于类的名字相同,并且不能指定返回类型。可以没有形参,也可以有多个形参
构造函数自动执行,只要创建该类型的一个对象,编译器就运行一个构造函数。
构造函数不能声明为const,创建类类型的const对象时,运行一个普通的构造函数来初始化该const对象。
例:
class Sales_item {
public:
Sales_item() const; //error
};
const Sales T;
与其他函数不同,构造函数可以包含一个初始化列表,构造函数可以定义在类的内部或外部。
构造函数初始化式只在构造函数的定义中而不是声明中指定。
不管成员是否在构造函数初始化列表中显式的初始化,类类型的数据成员总是在初始化阶段初始化。初始化发生在计算阶段开始之前。
使用构造函数初始化列表的版本初始化数据成员,发生在初始化阶段。函数体内对数据成员赋值的发生在函数计算执行阶段。
有些成员必须在构造函数初始化列表中进行初始化。对于这样的成员,在构造函数体中对他们赋值不起作用。
没有默认构造函数的类类型的成员,以及const或引用类型的成员,不管是哪种类型,都必须在构造函数初始化列表中进行初始化。
按照与成员声明一致的次序编写构造函数初始化列表是个好主意。此外,尽可能避免使用成员来初始化其他成员。
类通常应定义一个默认构造函数。假定有一个NoDefault类,定义了一个接受string实参的构造函数,则编译器不在合成默认构造函数。
1.必须通过传递一个初始化的string值给NoDefault的构造函数来显式的初始化NoDefault成员
2.编译器不再默认合成默认构造函数。
3.此类型不能用作动态分配数据的元素类型。
4.此类型的静态分配数组必须为每个元素提供一个显式的初始化式。
5.如果有一个保存NoDefault对象的容器,例如vector,就不能使用接受容器大小而没有同时提供一个元素初始化式的构造函数。
实际上,如果定义了其他构造函数,则提供一个默认构造函数几乎总是对的。
使用构造函数:
Sales_item myobj;
Sales_item myobj = Sales_item();
Sales_item myobj();错误
隐式类类型转换。可以用单个实参来调用的构造函数定义了从形参类型到该类类型的一个隐式转换。
class Sales_item {
public:
Sales_item(const std::string &book=""):
isbn(book), units_sold(0), revenue(0.0) {}
Sales_item(std::istream &is);
};
string null_book = "999";
item.same_isbn(null_book);
same_isbn本身期待一个Sales_item的参数,此时发生隐式类型转换,null_book调用构造函数,生成一个新的Sales_ttem对象(临时对象),被传递给了same_sibn。
一旦same_isbn结束,就不能再访问此临时对象,我们构造了一个在测试完成后被丢弃的对象,这个行为几乎肯定是一个错误。
抑制由构造函数定义的隐式转换,可以通过将构造函数声明为explicit,来防止在需要隐式转换的上下文中使用构造函数。
explicit关键字只能用于类内部的构造函数声明上。在类的定义体外部所做的定义上不再重复它。重复会报错。
class Sales_item {
public:
explicit Sales_item(const std::string &book=""):
isbn(book), units_sold(0), revenue(0.0) {}
explicit Sales_item(std::istream &is);
};
string null_book = "999";
item.same_isbn(null_book); //此时编译时会报错。
为转换而显式的使用构造函数
item.same_isbn(Sales_item(null_book));
通常,除非有明显的理由想要定义隐式转换,否则,单形参构造函数应该为explicit。
类成员的显式初始化,这是从c中继承而来的,有三个重大的缺点:
1.要求类的全体数据成员都是public。
2.将初始化每个对象的每个成员的负担放在程序员身上。乏味且易于出错的,因为容易遗忘初始化式或提供不适当的初始化式。
3.如果增加或删除一个成员,必须找到所有的初始化并正确更新。
友元,在某些情况下,允许特定的非成员函数访问一个类的私有成员,同时仍然阻止一般的访问,这是很方便做到的。
友元机制允许一个类将对其非公有成员的访问权授予指定的函数或类。友元的声明关键字friend。
它只能出现在类定义的内部,友元的声明可以出现在类中的任何地方。
通常,将友元声明成组的放在类定义的开始或结尾是个好主意。
友元类,友元函数。
定义友元类时,友元类必须先被定义过。
友元声明将已命名的类或非成员函数引入到外围作用域中。此外,友元函数可以在类的内部定义,该函数的作用域扩展到包围该类定义的作用域。
static数据成员独立于该类的任意对象而存在,是与类关联的对象。
static成员函数没有this形参,它可以直接访问所属类的static成员,但不能访问非static成员。
使用类的static成员的优点:
1.static成员的名字在类的作用域中,因此可以避免与其他类的成员或全局对象名字冲突。
2.可以实施封装,static成员可以是私有成员,而全局变量不可以。
3.通过阅读程序容易看出static成员是与特定类关联的。
在类的外部定义static成员时,无须重复指定static保留字,该保留字只出现在类定义体内部的声明处。
static成员函数没有this指针,因为它不属于任何对象的组成部分。
static关键字只能用于类定义体内部的声明中,定义不能标示为static。
double Account::interestRate = initRate();
其中interestRate在类中是static声明修饰,在类外定义时不能再加static,initRate是类中的私有函数,前面已经有类作用域的声明,后面可以直接使用类中的initRate函数。
static在类中声明变量,在类外定义变量,存储空间开辟在数据段上。
例外,const static修饰的变量可以在类体中直接赋值。但也需要在类外定义此变量的存储空间。定义时不需要再赋值。
static成员函数不与对象绑定,没有this指针
第十三章
拷贝(复制)构造函数:是一种特殊的构造函数,具有单个形参,该形参(常用const修饰)是对该类类型的引用。
当定义一个新对象并用一个同类型的对象对它进行初始化时,将显式使用拷贝构造函数。当将该类型的对象传递给函数或从函数返回该类型对象时,将隐式使用拷贝构造函数。
析构函数是构造函数的互补:当对象超出作用域或动态分配的对象被删除时,将自动应用析构函数。析构函数可用于释放对象所获取的资源。
不管类是否定义了自己的析构函数,编译器都自动执行类中非static数据成员的析构函数。
与默认构造函数一样,拷贝构造函数可以由编译器隐式调用。拷贝构造函数可用于:
1.根据另一个同类型的对象显式或隐式初始化一个对象。
2.复制一个对象,将它作为实参传给一个函数。
3.从函数返回时复制一个对象。
4.初始化顺序容器中的元素。
5.根据元素初始化式列表初始化数组元素。
默认合成的拷贝构造函数,执行逐个成员初始化,将新对象初始化为原对象的副本。
数组是个例外,虽然一般不能直接复制数组,但如果一个类具有数组成员,则合成拷贝构造函数将复制整个数组。
因为用于向函数传递对象和从函数返回对象,该构造函数一般不应设置为explicit。
通常,定义拷贝构造函数最困难的部分在于认识到需要拷贝构造函数。只有能认识到需要拷贝构造函数,定义拷贝构造函数一般非常简单。
对象创建时用另一个对象初始化赋值,调用拷贝构造函数。
对象已经存在,赋值时,调用赋值函数。
如果我们不想实在不想编写拷贝构造函数和赋值函数,又不允许别人使用编译器默认提供的缺省函数怎么办?
只需要将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。因为编译器在默认调用这类函数时,发现其为私有成员函数,则编译报错。
为了防止复制,类必须显式声明其拷贝构造函数为private。
如果拷贝构造函数是私有的,将不允许用户代码复制该类类型的对象,编译器将拒绝任何进行复制的尝试。
然而,友元和成员函数可以进行复制。如果想连友元和成员函数中的复制也禁止,就可以声明一个private拷贝构造函数但不对其定义。
声明而不定义成员函数是合法的,但是,使用未定义成员的任何尝试将导致链接失败。
通过声明(但不定义)private拷贝构造函数,可以禁止任何复制类类型对象的尝试:用户代码中的复制尝试将在编译时标记为错误,而成员函数和友元中的复制尝试将在连接时导致错误。
一般来说,最好显式或隐式定义默认构造函数和拷贝构造函数。只有不存在其它构造函数时才合成默认构造函数。如果定义了拷贝构造函数,也必须定义默认构造函数。
大多数操作符可以定义为成员函数或非成员函数。当操作符为成员函数时,它的第一个操作数隐式绑定到this指针。因此,赋值操作符接受单个形参,且该形参是同一类类型的对象。右操作数一般为const引用传递。
例如:
Sales_item& Sales_item::operator=(const Sales_item &rhs)
{
成员逐个赋值。。。
* 删除已开辟空间。。。
申请新空间。。。
拷贝。。。
return *this;
}
一般而言,如果类需要拷贝构造函数,我们几乎肯定也需要赋值操作符。
构造函数的一个用途是自动获取资源。例如,构造函数可以分配一个缓冲区或打开一个文件,在构造函数中分配了资源之后,需要一个对应操作自动回收或释放资源。析构函数就是这样的一个特殊函数,它可以完成所需的资源回收,作为类构造函数的补充。
容器中的元素是按逆序撤销。
如果类需要析构函数,则它也需要赋值操作符合拷贝构造函数,这是一个有用的经验法则。这个规则常称为三法则(rule of three),指的是如果需要析构函数,则需要所有这三个复制控制成员。
合成析构函数按对象创建时的逆序撤销每个非static成员。
析构函数是个成员函数,它的名字是在类名字之前加上一个~,它没有返回值,没有形参。因为不能指定任何形参,所以不能重载析构函数。
析构函数与拷贝构造函数或赋值操作符之间的一个重要区别是,即使我们编写了自己的析构函数,合成析构函数仍然运行。先执行自己定义的析构函数,再执行合成析构函数。
理解:自己的析构函数销是合成析构函数的一个扩展补充,负责毁堆资源和关闭文件,合成析构函数负责销毁普通的数据成员。
管理指针成员:
1.指针成员采取常规指针行为。这样的类具有指针的所有缺陷但无需特殊的复制控制。
2.类可以实现所谓的“智能指针”行为。指针所指向的对象时共享的,但类能够防止悬垂指针。
3.类采取值型行为。指针所指向的对象时唯一的,由每个类对象独立管理。
智能指针?
悬垂指针:指针指向的存储空间已被释放。
第十四章 重载操作符与转换
重载操作符是具有特殊名称的函数:保留字operator后接需定义的操作符符号。像任意其他函数一样,重载操作符具有返回类型和形参表,如下语句:
Sales_item ooperator+(const Sales_itme& , const Sales_item&);
不可重载的操作符
:: .* . ?:
其它的操作符都可以重载。
重载操作符必须具有至少一个类类型或枚举类型的操作数。这条规则强制重载操作符不能重新定义用于内置类型对象的操作符的含义。
优先级和结核性是固定的。
不再具备短路求值特性。
类成员与非成员,大多数重载操作符可以定义为普通非成员函数或类的成员函数。
作为类成员的重载函数,其形参看起来比操作数数目少1.作为成员函数的操作符有一个隐含的this形参,限定为第一个操作数。
一般将算数和关系操作符定义为非成员函数,而将赋值操作定义为成员。
重载操作符的设计。
1.不要重载具有内置含义的操作符。重载逗号,取地址,逻辑与、逻辑或等操作符通常不是好做法。这些操作符具有有用的内置含义,如果我们定义了自己的版本,就不能再使用这些内置含义。
2.大多数操作符对类对象没有意义。一般相等测试应使用operator==;输入输出使用移位操作符;测试对象是否为空使用operator!。
3.如果一个类有算数操作符或位操作符,那么提供相应的复合赋值操作符一般是个好做法。
4.相等和关系操作符,关联容器应该定义== 、<操作符,许多算法假定这些操作符存在。如果定义了==,则应定义!=,如果定义了<,则应定义(>,>=,<,<=)。
5.选择成员或非成员实现。
原则:
=、下标[]、调用()和成员访问->等操作符必须定义为成员函数,否则编译报错。
复合赋值操作符通常定义为类的成员。与赋值不同的是,不一定非得这样做,编译不报错。
改变对象状态或与给定类型紧密联系的其它一些操作符,如自增自减和解引用,通常定义为成员函数。
对称的操作符,如算数操作符、相等操作符、关系操作符和位操作符,最好定义为普通非成员函数。
当一个重载操作符的定义不明显时,给操作取一个名字更好。对于很少用的操作,使用命名函数通常也比用操作符更好。如果不是普通操作,没有必要为简洁而使用操作符。
输入输出IO操作必须为非成员函数。否则,左操作数只能是该类类型的对象:
Sales_item Item;
item << cout;
这个用法与为其他类型定义的输出操作符的正常使用方式相反。
输出操作符通常所做格式化应尽量少,并且不应该输出换行符。
输入操作符>>的重载,必须处理错误和文件结束的可能性。
设计输入操作时,如果可能,要确定错误恢复措施,这很重要。
如果既定义了算数操作符又定义了相关复合赋值操作符的类,一般应用复合赋值实现算数操作符。如+,用+=去实现,比其它方式更简单且更有效,不必创建和销毁一个临时来保存+的结果。
赋值必须返回对*this的引用。
类定义下标操作符时,一般需要定义两个版本:一个为非const成员并返回引用,另一个为const成员并返回const引用。
为了支持指针类型,例如迭代器,c++语言允许重载解引用操作符*和箭头->。
->必须定义为类成员函数。*不要求定义为成员,但将它作为成员一般也是正确的。
区别前缀++和后缀++,后缀式操作符函数接受一个额外的(无用的)int型形参。前缀不需要参数。
调用函数()重载???
第十五章 面向对象编程
面向对象编程基于三个基本概念:数据抽象、继承和动态绑定。在c++中用类进行数据抽象,用类派生从一个类继承另一个类:派生类继承基类的成员。动态绑定使编译器能够在运行时决定使用基类中定义的函数还是派生类中定义的函数。
多态性紧用于通过继承而相关的类型的引用或指针。
在c++中,基类必须指出希望派生类重定义哪些函数,定义为virtual的函数是基类期待派生类重新定义的,基类希望派生类继承的函数不能定义为虚函数。
动态绑定,我们能够编写程序使用继承层次中任意类型的对象,无须关心对象的具体类型。使用这些类的程序无须区分函数是在基类还是在派生类中定义的。
通过基类的引用(或指针)调用虚函数时,发生动态绑定。引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指对象的实际类型所定义的。
protected成员不能被类的用户访问,可以被该类的派生类访问。
派生类只能通过派生类对象访问其基类的protected成员,派生类对其基类类型对象的protected成员没有特殊访问权限。
为了定义派生类,使用类派生列表指定基类。派生列表指定了一个或多个基类,具有如下形式:
class classname: access-lable base-class
这里access-lable是public、protected、private,base-class是已定义的类的名字。类派生列表可以指定多个基类。继承单个基类最为常见。
尽管不是必须这样做,派生类一般会重定义所继承的虚函数。如果派生类没有重定义某个虚函数,则使用基类中定义的版本。
派生类型必须对想要重定义的每个继承成员进行声明。
派生类中虚函数的声明必须与基类中的定义方式完全匹配,但有一个例外:返回对基类型的引用(或指针)的虚函数。派生类中的虚函数可以返回基类函数所返回类型的派生类的引用(或指针)。
如:Item_base类可以定义返回Item_base*的虚函数,如果这样,派生类Bulk_item类中定义的实例可以定义返回Item_base*或Bulk_item*。
一旦函数在基类中声明为虚函数,它就一直未虚函数,派生类无法改变该函数为虚函数这一事实。派生类重定义虚函数时,可以使用virtual保留字,但不是必须这样做。
c++语言不要求编译器将对象的基类部分和派生类部分连续排列。
派生类中的函数可以使用基类的成员。
用作基类的类必须是已定义的。
从效果来说,最底层的派生类对象包含其每个直接基类和间接基类的子对象。
c++中的函数调用默认不使用动态绑定。要触发动态绑定,必须满足两个条件:
1.只有指定为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数,非虚函数不进行动态绑定;
2.必须通过基类类型的引用或指针进行函数调用。
引用和指针的静态类型与动态类型可以不同,这是c++用以支持多态性的基石。
通过基类引用或指针调用基类中定义的函数时,我们并不知道执行函数的对象的确切类型,执行函数的对象可能是基类型的,也可能是派生类型的。
如果调用非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数。
如果调用虚函数,则直到运行时才能确定调用哪个函数,运行的虚函数是引用所绑定的或是指针所指向的对象所属类型定义的版本。
在编译时确定非virtual调用。
覆盖虚函数机制。在某些情况下,希望覆盖虚函数机制并强制函数调用使用虚函数的特定版本,这时可以使用域操作符:
例:
Item_base *basep = &derived; //derived是派生类的实例对象
double d = basep->Item_base::net_price(43);
只有成员函数中的代码才应该使用作用域操作符覆盖虚函数机制。
派生类虚函数调用基类版本时,必须显式使用作用域操作符。如果派生类函数忽略了这样做,则函数调用会在运行时确定并且将是一个自身调用,从而导致无穷递归。
虚函数与默认实参,虚函数也可以有默认实参。通常,如果有用在给定调用中的默认实参值,该值将在编译时确定。如果一个调用省略了具有默认值的实参,则所用的值由调用函数的类型定义,与对象的动态类型无关。通过基类的引用或指针调用虚函数时,默认实参为基类虚函数声明中指定的值,如果通过派生类的指针或引用调用虚函数,则默认实参是派生类的版本中声明的值。
在同一虚函数的基类版本和派生类版本中使用不同的默认实参几乎一定会引起麻烦。
如果通过基类的引用或指针调用虚函数,但实际执行的是派生类中定义的版本,这时可能会出现问题。
公用、私有和受保护的继承。
每个类控制它所定义的成员的访问。派生类可以进一步限制但不能放松对所继承的成员的访问。
如果是公有继承public,基类成员保持自己的访问级别。
如果是受保护继承protected,基类的public和protected成员在派生类中为protected成员。
如果是私有继承private,基类的所有成员在派生类中为private成员。
无论派生列表中是什么访问标号,所有派生类内部对基类成员具有相同的访问。派生类访问标号将控制派生类用户对从基类继承而来的成员的访问。
使用private或protected派生的类不继承基类的接口,相反,这些派生通常称为实现继承。派生类在实现中使用被继承类但继承基类的部分并未成为其接口的一部分。
迄今为止,最常见的继承形式是public。
派生类可以恢复继承成员的访问级别,但不能使访问级别比基类中原来指定的更严格或更宽松。使用using声明访问基类中的名字。
友元关系不能继承。基类的友元对派生类的成员没有特殊访问权限。如果基类被授予友元关系,则只有基类具有特殊访问权限,该基类的派生类不能访问授予友元关系的类。
如果基类定义了static成员,则整个继承层次中只有一个这样的成员。
每个派生类对象包含一个基类部分,所以存在从派生类型引用到基类类型引用的自动转换。
没有从基类引用(或指针)到派生类引用(或指针)的(自动)转换。
派生类构造函数,受继承关系影响,每个派生类构造函数除了初始化自己的数据成员之外,还要初始化基类。
构造函数初始化列表为类的基类和成员提供初始值,它并不指定初始化的执行次序。首先初始化基类,然后根据声明次序初始化派生类的成员。
只能初始化直接基类。直接基类就是在派生列表中指定的类。
重构包括重新定义类层次。为了适应应用程序的需要而重新设计类以便增加新函数或处理其他改变时,最有可能需要进行重构,重构常见在面向对象应用程序中非常常见。
如果析构函数为虚函数,那么通过指针调用时,运行哪个析构函数将因指针所指对象类型的不同而不同。
即使析构函数没有工作要做,继承层次的根类也应该定义一个虚析构函数。
构造函数不是虚函数。构造函数是在对象完全构造之前运行的,在析构函数运行的时候,对象的动态类型还不完整。
将类的赋值操作符设为虚函数很可能令人混淆,而且不会有什么用处。
与基类成员同名的派生类成员将屏蔽对基类成员的直接访问。使用作用域操作符访问被屏蔽的成员。
在派生类中使用同一名字的成员函数,其行为与数据成员一样,即使函数原型不同,基类成员也会被屏蔽。
如果派生类重定义了重载成员,则通过派生类型只能访问派生类中重定义的那些成员。
如果派生类想通过自身类型使用所有的重载版本,则派生类必须要么重定义所有重载版本,要么一个也不重定义。
否则使用using声明???
纯虚函数,在函数形参表后面写上=0.
含有(或继承)一个或多个纯虚函数的类是抽象类。除了作为抽象基类的派生类的对象的组成部分,不能创建抽象类的对象。
第十六章 模板与泛型编程
面向对象编程中的多态性在运行时应用于存在继承关系的类。我们能够编写使用这些类的代码,忽略基类与派生类之间类型上的差异。
只要使用基类的引用或指针,基类类型或派生类类型的对象就可以使用相同的代码。
运行时多态性,编译时多态性。
在泛型编程中,我们所编写的类和函数能够多态的用于跨越编译时不相关的类型。一个类或一个函数可以用来操作多种类型的对象。
c++中模板是泛型编程的基础。
模板定义以关键字template开始,后接模板形参表,模板形参表是用尖括号括住的一个或多个模板形参的列表,形参之间以逗号分隔。
模板形参表不能为空。
template <typename T>
int compare(const T &v1, const T &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
模板形参可以是表示类型的类型形参,也可以是表示常量表达式的非类型形参。
使用函数模板时,编译器会推断哪个模板实参绑定到模板形参。
一旦编译器确定了实际的模板实参,就称它实例化了函数模板的一个实例。
推导出实际模板实参后,编译器使用实参代替相应的模板形参产生并编译该版本的函数。
编译器承担了为我们使用每种类型而编写函数的单调工作。
inline函数模板,需放在模板形参表之后,返回类型之前,不能放在template之前。
每个模板类型形参前必须带上关键字class或typename,每个非类型形参前面必须带上类型名字,省略关键字或类型说明符是错误的。
例如:
template <typename T, U> T calc(const T&, const U&);
类型形参由关键字class或typename后接说明符构成。这两个关键字具有相同的含义,都指出后面所接的名字表示一个类型。
typename代替关键字class更直观,毕竟,可以使用内置类型作为实际的类型形参。
模板类型形参可以用于指定返回类型或函数形参类型,以及在函数体中用于变量声明或强制类型转换。
通过在成员名前加上关键字typename作为前缀,可以告诉编译器将成员当做类型。
例:
template <class Parm, class U>
Parm fcn(Parm* array, U value)
{
typename Parm::size_type *p; //类型or成员?
}
非类型模板形参。在调用非类型形参将用值代替,值的类型在模板形参表中指定。
例:
template <class T, size_t N> void array_init(T (&parm)[N])
{
for (size_t i = 0; i != N; ++i)
parm[i] = 0;
}
通过将形参设为const引用,就可以使用不允许复制的类型。大多数类型(包括内置类型,除IO类型之外)都允许复制。但是也有不允许复制的类类型。将形参设为const引用,保证这种类型可以用于compare函数,而且,如果有比较大的对象调用compare,则这个设计还可以运行的更快。
模板是一个蓝图,它本身不是类或函数。编译器用模板产生指定的类或函数的特定类型版本。产生模板的特定类型实例的过程称为实例化。
类模板的每次实例化都会产生一个独立的类类型。与其他任意实例类型没有关系。
类模板的形参是必须的。
Queue qs; 错误
Queue<int> qi;
Queue<string> qs;
Queue不是类型
使用函数模板时,编译器通常会为我们推断模板实参:
int main()
{
compare(1,0);
compare(3.14, 2.7);
return 0;
}
多个类型形参的实参必须完全匹配。如果推断类型不匹配,则调用会出错。
一般而言,不会转换实参以匹配已有的实例化,相反,会产生新的实例。除了产生实例之外,编译器只会执行两种转换:
1.const转换,接受const引用或const指针的函数可以分别用非const对象的引用或指针来调用,无须产生新的实例化。
2.数组或函数到指针的转换,如果模板形参不是引用类型,则对数组或函数类型的实参应用常规指针转换。
模板实参推断与函数指针,可以使用函数模板对函数指针进行初始化或赋值,编译器使用指针的类型实例化具有适当模板实参的模板版本。
例如:
template <typename T> int compare(const &T, const &T);
int (*pf1)(const int &, const int &) = compare;
Queue类实现
template <class Type> class Queue {
public:
Queue() : head(0), tail(0) { }
Queue(const Queue &Q) : head(0),tail(0) {copy_elems(Q);}
Queue& operator=(const Queue&);
~Queue() {destroy();}
Type &front() {return head->item;}
const Type &front() const {return head->item;}
void push(const Type &);
void pop();
bool empty() const { return head == 0; }
private:
QueueItem<Type> *head;
QueueItem<Type> *tail;
void destroy();
void copy_elems(const Queue&);
};
通常,当使用类模板的名字时候,必须指定模板形参。这一规则有个例外,在类本身的作用域内部,可以使用类模板的非限定名。
如Queue是Queue<Type>的缩写。
类模板成员函数的定义具有如下形式:
1.必须以关键字template开头,后接类的模板形参表。
2.必须指出它是哪个类的成员。
3.类名必须包含其模板形参。
如:template <class T> ret-type Queue<T>::member-name
如:
template <class Tyte> void Queue<Type>::destroy()
{
while (!empty())
pop();
}
非类型模板实参必须是编译时常量表达式。
template <int hi, int wid> class Screen {};
Screen<24, 80>hp2621;
hi的模板实参是24,wid的模板实参是80.
类模板中的友元声明:
1.普通非模板类或函数的友元声明,每一种都声明了一个或多个实体的友元关系。
2.类模板或函数模板的友元声明,授予对友元所有实例的访问权。
3.只授予对类模板或函数模板的特定实例的访问权的友元声明。
模板特化
函数模板特化:
1.关键字template后面接一对空的尖括号<>;
2.再接模板名和一对尖括号,尖括号中指定这个特化定义的模板形参;
3.函数形参表。
4.函数体。
处理C风格的字符串,如:
template <> int compare<const char *>(const char * const &v1, const char* const &v2)
{
return strcmp(v1,v2);
}