可能延长对象的生命期:
shared_ptr是强引用,只要有一个指向x对象的shared_ptr存在,该对象就不会析构。
比如java的观察者模式中如果主题没有主动去unrigister一个observer,那么那个observer永远不会被释放。
比如boost::bind会把实参拷贝一份,样例代码如下:
class Foo{void doit();};
shared_ptr<Foo> pFoo(new Foo);
boost::function<void()> func = boost::bind(&Foo::doit, pFoo);
这里的func对象就持有一份shared_ptr<Foo>的拷贝,可能延长倒数第二行创建的对象的生命期。
析构动作在创建时被捕获,这意味着:
虚析构函数不是必需的了;
shared_ptr<void>可以持有任何对象,而且能安全释放;
cprimeplus读书笔记:
逗号运算符有两个特性:
首先,它确保先计算第一个表达式,后计算第二个表达式,也就是说逗号是一个顺序点。
其次,逗号表达式的值是第二部分的值,也就是最后的值,比如a={120,123},最后a的值是123。
假设pf为函数指针,为何c++中pf和*pf等价呢?
一种学派认为,由于pf是函数指针,而*pf是函数,因此应将(*pf)()用作函数调用。另一种学派认为,由于函数名是指向该函数的指针,指向函数的指针的行为应与函数名相似,因此应将pf()用作函数调用使用。C++进行了折衷——这2种方式都是正确的,或者至少是允许的,虽然他们在逻辑上是互相冲突的。
auto只能用于单值初始化,不能用于初始化列表。
第三代具体化(ISO/ANSI C++标准):
1.对于给定的函数名,可以有非模板函数、模板函数和显式具体化模板函数以及它们的重载版本。
2.显式具体化的原型和定义应以template<>打头,并通过名称来指出类型。
3.具体化优先于常规模板,而非模板函数优先于具体化和常规模板
下面是用于交换job结构的非模板函数,模板函数和具体化的原型:
//非模板函数
void Swap(job &, job&);
//模板函数
template<typename T>
void Swap(T &, T &);
//具体化的原型
template<> void Swap<job>(job &, job &);
实例化和具体化:
隐式实例化:在代码中包含函数模板本身并不会生成函数定义,它只是一个用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板实例。模板并非函数定义,但使用int的模板实例是函数定义。这种实例化方案被称为隐式实例化,因为编译器之所以知道需要进行定义,是由于程序调用Swap()函数时提供了int参数。
显式实例化:最初编译器只能通过隐式实例化来使用模板生成函数定义,但现在C++还允许显式实例化,这意味着可以直接命令编译器创建特定的实例,如Swap<int>()。其语法是,声明所需的种类——用<>符号指示类型,并在声明前加上关键字template:template void Swap<int>(int, int);实现了这种特性的编译器看到上述声明后,将使用Swap()模板生成一个使用int类型的实例。也就是说,该声明的意思是“使用Swap()模板生成int类型的函数定义”。(个人看法,编译器的这个功能纯粹糟粕,这个功能的目的应该是为了节约生成函数的时间吧(我觉得这样可以编译期生成该函数,如果不是的话,当我没说),完全可以用具体化来替代它,根本不值得复杂化编译器和语法。)
显式具体化:与显式实例化不同的是,显式具体化使用下面两个等价的声明之一:template<>void Swap<int>(int &, int &);template<> void Swap(int &, int &);区别在于,这些声明的意思是“不要使用Swap()模板来生成函数定义,而应使用专门为int类型显式地定义的函数定义”。这些原型必须有自己的函数定义。显式具体化声明在关键字template后包含<>,而显式实例化没有。(个人看法,这个功能看似有用其实也不太有价值,只能说可能极特殊的情况下会有用,因为C++已经提供了运算符重载了,同名函数本就应该实现的目的相同,那么就没理由会出现函数代码不同的情况了。)
隐式实例化、显式实例化和显式具体化统称为具体化。它们的相同之处在于,它们表示的都是使用具体类型的函数定义,而不是通用描述。
警告:试图在同一个文件(或转换单元)中使用同一种类型的显式实例和显式具体化将出错。
转化单元:待了解。
double&类型的形参不能指向int类型的变量(因为double类型的形参可以指向int类型的变量,所以特意提一下引用类型以免误用)
重载解析:
对于函数重载、函数模板和函数模板重载,C++需要(且有)一个定义良好的策略,来决定为函数调用使用哪一个函数定义,尤其是有多个参数时,这个过程称为函数解析。详细解释这个策略需要将近一章的篇幅,因此我们先大致了解一下这个过程是如何进行的:
第一步:创建候选函数列表。其中包含与被调用函数的名称相同的函数和函数模板
第二步:使用候选函数列表创建可能函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包含实参类型与相应的形参类型完全匹配的情况。
第三步:确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。通常,从最佳到最差的顺序如下所述:
1.完全匹配,但常规函数优先于模板。
2.提升转换(例如,char和shorts自动转换为int,float自动转换为double)。
3.标准转换(例如,int转换为char,long转换为double)。
4.用户定义的转换,如类声明中定义的转换。
其中,3和4具体什么意思不太了解,需要进一步加深了解。
然后尴尬的地方在于,有n个形式参数,那么就有可能有3^n个完全匹配:函数test(int), test(int &), test(const int &)可以同时存在。指出一点,cprimeplus中文版书上说函数test(blot)和函数test(blot &)可以共存(其中blot是结构体),但是我在ubuntu16.04用g++编译是无法通过的,无论是C++还是C++11。
然后以下属于完全匹配中的无关紧要的转换:
但是对于这个表的细节地方,我保留看法,比如Type *往const Type的转化,怎么看都像是作者笔误。
不过这地方其实也不用太深究,毕竟绝大多数时候你都不会这么设计函数。所以再往深的地方我暂时不去了解,以后有空闲可以再去了解一下。
decltype(C++11)关键字:
该关键字用法如下:decltype(x) y;//使y具有和x相同的类型
从而解决的一个可能在模板函数中出现的问题:
template<typename T1, typename T2>
void ft(T1 x, T2 y) {
...
decltype(x+y)xpy = x + y;
...
}
显然没有这个关键字也有解决方法,那就是不用定义xpy这个中间变量,直接以x+y的形式使用它就好,这样的话最大的问题在于可能有很多中间变量不能定义导致最终的表达式很长,影响代码的可读性和正确性。而且该关键字可以和typedef一起使用。
其中对于声明decltype(expression)var, decltype的核对表如下:
第一步:如果expression是一个没有用括号括起来的标志符,则var类型与该表支付的类型完全相同,包括const等限定符。
第二步:如果expression是一个函数调用,则var的类型与函数的返回类型相同,但是编译器并不需要实际调用该函数。
第三步:如果expression是一个左值,则var为指向其类型的引用,但是没有括号的标志符在第一步已经处理过了。
第四步:如果前面的条件都不满足,则var的类型与expression的类型相同。
注意:auto很多情况下应该可以替代decltype吧,但是auto毕竟还有限制,具体限制还得回头了解。
C++11后置返回类型:
上面说的解决方案没法解决返回x+y的问题,书上说是因为确定返回值类型的时候还没有变量x和y呢(所以以后还得了解一下函数声明过程中各个变量、类型等出现的顺序),这个用auto也解决不了(但是突然发现g++还有C++14的编译设定,用了就可以使用auto解决这个问题了),需要用到后置返回类型,使用方法如下:
template<typename T1, typename T2>
auto ft(T1 x, T2 y)-> decltype(x+y) {
...
return x+y;
}
register关键字的目的是告诉编译器它修饰的变量用的很多,希望编译器能对该关键字做优化,例如放进cpu寄存器。不过C++11中它已经没有这个作用了。
静态持续变量:
1.C++为静态存储持续性变量提供了三种链接性:外部链接性(可在其他文件中访问)、内部链接性(只能在当前文件中访问)和无链接性(只能在当前函数或代码块中访问)。对应的声明方式分别时全局变量,静态全局变量,静态局部变量。
2.静态变量的树木在程序运行期间是不变的,因此不需要特殊的装置(如栈)来管理他们,只需要固定的内存块来存储所有的静态变量。
3.如果没有显式的初始化静态变量,编译器将把它设置为0。默认情况下,静态数组和结构将每个元素或成员的所有位都设置为0。
使用关键字extern进行引用声明的时候不能初始化,不然就会声明为定义,导致分配存储空间。
可以在一个文件中声明static int var的同时在另一个文件中声明int var。
关键字volatile表明,即使程序代码没有对内存单元进行修改,其值也可能发生变化,原因是指针指向的内存位置与其他程序或硬件等共享了。所以使用该关键字之后编译器就知道必须每次都去指针指向的内存去读取数据才能保证准确。
可以用mutable之处,即使结构(或类)变量为const,其某个成员也可以被修改。例如如下代码:
struct data { char name[30]; mutable int accesses; ...}
const data veep = { "adbc", 0, ...};
strcpy(veep.name, "aedf"); //不允许修改,错误
veep.accesses++; //允许修改,正确
在C++中,const限定符对默认存储类型稍有影响。默认情况下全局变量的链接性是外部的,但const全局变量的链接性是内部的。所以可以在头文件中定义const变量。可以通过extern关键字来覆盖默认的内部链接性:extern const int states = 50;
和C语言中一样,C++不允许在一个函数中定义另外一个函数,因此所有函数的存储持续性都自动为静态的,即在整个程序执行期间都一直存在。在默认情况下,函数的链接性为外部的,即可以在文件中共享。实际上,可以在函数原型中使用extern指出函数是在另一个文件中定义的,不过这时可选的。还可以使用static将函数的链接性设置为内部的,使之只能在一个文件中使用。必须同时在函数原型和函数定义中使用该关键字。这意味着该函数只能在这个文件中可见,还意味着可以在其他文件中定义同名的函数,就和变量一样。
内联函数不受单定义规则限制,所以可以在头文件中定义内联函数,但是C++要求同一个函数的所有内联定义都必须相同。
C++在哪里查找函数:假设在程序的某个文件中调用一个函数,如果该文件中的函数原型指出该函数是静态的,则编译器只会在该文件中查找函数定义;否则,编译器将在所有的程序文件中查找。如果找到两个定义,编译器将发出错误信息,因为每个外部函数只能有一个定义。如果在程序文件中没有找到,编译器将在库中搜索。这意味着如果定义了一个与库函数同名的函数,编译器将使用程序员定义的版本,而不是库函数(然而,C++保留了标准库函数的名称,即程序员不应使用它们)。
语言链接性:在C语言中,一个名称只对应一个函数,为满足内部需要,C语言编译器可能将spiff函数名翻译成_spiff,这种方法被称为C语言链接性。但是在C++中,同一个名称可能对应多个函数,必须将这些函数翻译为不同的符号名称,因此C++编译器执行名称矫正,为重载函数生成不同的符号名称,例如将spiff(int)翻译成_spoff_i,这种方法被称为C++语言链接。
为了解决C语言的语言链接性和C++的语言链接性不同的问题,可以用函数原型来指出要使用的约定:
extern "C" void spiff(int); //use C protocol for name look-up
extern void spoff(int); //use C++ protocol for name look-up
extern "C++" void spaff(int); //use C++ protocol for name look-up
定位new运算符:这种使用方式可以制定要使用的位置,使用时需要包含头文件new,使用方法如下:
char buffer[200];
int main() {p = new (buffer) int[20];
这种方式是否可以使用局部变量书上没说,有时间再试一下。
这个例子中buffer处于静态存储去,不在delete的管理范围内,所以不能delete。默认定位new运算符基本上只是返回内存地址并强制转化成void*,也就是说基本上用的时候程序员需要自己重载,不然就基本没用。
new的工作原理:new其实是调用一个new()函数:
int * p = new int; //invokes new(sizeof(int))
int * p2 = new (buffer) int; //invokes new(sizeof(int), buffer)
int * p3 = new (buffer) int[40]; //invokes new(sizeof(int)*40, buffer)
using使用变量方法如下:
namespace Jill {double fetch;}
char fetch;
int main() {
using Jill:fetch; //put fetch into local namespace
doubel fetch; //Error! Already have a local fetch
cin >> fetch;
cin >> ::fetchl //read a value into global fetch
定义位于类声明中的函数都将自动成为内联函数,这应该就是g++编译器允许每个源文件都声明一个在类的声明中定义所有成员函数的类的原因了。
内联函数的特殊规则要求在每个使用它们的文件中都对其进行定义,所以最简便的方法是将内联定义放在头文件中(有些开发系统包含只能链接程序,允许将内联定义放在一个独立的实现文件中,但是为了兼容性,就应该定义在声明该函数的头文件中,话说这样的话编译器应该需要保证不会把inline关键字优化掉)。
当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数。
即使对于有默认构造函数的类Stock,下面的定义Stock second();也是定义了一个函数而不是一个对象。
Stock stock1("adsf", 12, 20.0);将创建一个对象stock1并执行初始化函数;Stock stock2 = new Stock("adsf", 12, 20.0);将创建一个临时对象并复制给stock2,然后Cprimeplus又指出也可能不会创建临时对象,那么不创建临时对象应该是因为编译器优化。。。。
C++中定义作用域为类的常量:
1.在类中声明一个枚举:class Bakery{private: enum {Months = 12}; double costs[Months];}。用这种方式声明枚举并不会创建类数据成员。也就是说,所有对象中都不包含枚举。另外,Months只是一个符号名称,在作用域为整个类的代码中遇到它时,编译器将用12来替换它。
2.使用关键字static:class Bakery {private: static const int Months = 12; double consts[Months];},该常量将与其他静态变量存储在一起,而不是存储在对象中。在C++98中,只能使用这种技术声明值为整数或枚举的静态变量,而不能存储double常量。C++11消除了这种限制。
作用域内枚举(C++11):解决传统的枚举中两个枚举类型中枚举量发生冲突的问题。
enum class egg {Small};enum class t_shirt {Small};
egg choice = egg:Small; t_shirt Floyd = t_shirt:Small;
其中关键字class可以替换成struct。同时作用域内枚举不能隐式转换成整型,但是可以进行显式的转化。
重载运算符的限制:
1.重载后的运算符必须至少有一个操作数是用户定义的类型,这将防止用户为标准类型重载运算符。因此,不能将-重载为计算两个double值的和。
2.使用运算符时不能违反运算符原来的句法规则。例如,不能将%重载成使用一个操作数。同样,不能修改运算符的优先级。
3.不能创建新运算符。例如,不能定义operator**()函数来表示求幂。
4.不能重载下面的运算符:
sizeof sizeof运算符
. 成员运算符
.* 成员指针运算符(啥意思。。)
:: 作用域解析运算符
? 条件运算符
typeid 一个RTTI运算符
const_cast,dynamic_cast,reinterpret_cast,static_cast 强制类型转换运算符。
5.下面的运算符只能通过成员函数进行重载:
= 赋值运算符
() 函数调用运算符
[] 下表运算符
-> 通过指针访问类成员的运算符
友元函数:通过让函数成为类的友元,可以赋予该函数与类的成员函数相同的访问权限。
创建友元函数的第一步时将其原型放在类生命中,并在原型声明前加上关键子friend。
友元函数的一个应用场所:如果要为类重载运算符,并将非类的项作为其第一个操作数,则可以用友元函数来反转操作数的顺序。
友元函数得声明在类的内部,所以就可以认为友元函数还是由类来控制,所以完全不算破坏OOP。
重载<<操作符,此时该函数必须是类Time的友元函数,因为理论上t.hours和t.minutes一定是私有成员变量:
ostream & operator<<(ostream & os, const Time &t) { os << t.hours << " hours, " << t.minutes << " minutes"; return os;}
转换函数:
转换函数必须是类方法;
转换函数不能指定返回类型;
转换函数不能有参数;
转换函数声明示例:
operator double();
编译器一般不会承担为有二义性的转换做决断的责任,所以有二义性的转换往往需要显式转换。
C++11中可以使用explicit修饰转换函数。
警告:应谨慎的使用隐式转换函数。通常,最好选择仅在被显式的调用时才会执行的函数。
C++自动提供下面这些成员函数:
默认构造函数,如果没有定义构造函数;
默认析构函数,如果没有定义;
复制构造函数,如果没有定义;
赋值运算符,如果没有定义;
地址运算符,如果没有定义;
更准确地说,编译器将生成上述最后三个函数的定义——如果程序使用对象的方式要求这样做。例如,如果将一个对象赋值给另一个对象,编译器将提供赋值运算符的定义。
何时调用复制构造函数:
新建一个对象并将其初始化为同类现有对象时。如String a(b); Sting a = b; String a = String(b); String *a = new String(b);
每当程序生成对象副本时。如函数按值传递对象或函数返回对象时。
供非const对象使用的操作符重载函数:char & String::operator[](int i);
仅供const对象使用的操作符重载函数:const char & String::operator[](int i) const;
静态类成员函数:
1.不能通过对象调用静态成员函数;实际上,静态成员函数甚至不能使用this指针。如果静态成员函数是在公有部份声明的,则可以使用类名和作用域解析运算符来调用它。
2.由于静态成员函数不与特定的对象相关联,因此只能使用静态数据成员。
3.可以使用静态成员函数设定类级(classwide)标记,以控制某些类接口的行为。例如,类级标记可以控制显式类内容的方法所使用的形式。
在使用定位new运算符创建变量时,由于不能用delete释放该变量,而用delete释放该变量使用的缓存不会调用该变量的析构函数,所以如果该变量的构造函数内部还会使用new申请别的内存的话,就必须显式的调用析构函数,然后那个变量对应的内存才可以重新使用,否则会造成内存泄漏。
公有派生:基类中的公有成员将成为派生类中的公有成员;基类中的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问。
虚析构函数:如果析构函数不是虚的,则将只调用对应于指针类型的析构函数。如果析构函数是虚的,将调用相应对象类型的析构函数。
重新定义基类中的虚函数:重新定义不会生成函数的两个重载版本,而是隐藏了基类中的虚函数。如virtual void Driver::a()会隐藏virtual void Base::a(int)。这引出来了两条经验规则:
1.如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针(这种例外是新出现的)。这种特性被称为返回类型协变(covariance of return type),因为允许返回类型随类类型的变化而变化。注意这种例外只适用于返回值而不适用于参数。
2.如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。例子如下:
class Base{public: virtual void a(int a)const; virtual void a(double a)const;};
如果派生类只重新定义一个版本,则另外两个版本将被隐藏,派生类对象将无法使用它们。比如只重新定义了virtual void a(int a)const;则无法使用另一个函数virtual void a(double a)const;。或者准确地说如果定义了一部分重载的版本,则剩下的没有定义的重载的版本就无法使用,但是如果一个都没有定义的话,则可以继承基类中的所有的重载的版本(显然为了防止以后出错应该立马定义所有的重载版本而不是一个都不定义)。
protected:对于外部世界来说,保护成员的行为与私有成员类似;但对于派生类来说,保护成员的行为与公有成员相似。
私有继承:私有继承是C++实现has-a关系的另一种途径。使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味着基类方法将不会成为派生对象公有接口的一部分,但可以在派生类的成员函数中使用他们:
访问基类方法:使用作用域解析运算符;
访问基类对象:使用强制类型转换;
访问基类的友元函数:显式的转换为基类来调用正确的函数;
私有继承的优势:可以使用保护成员,可以重新定义虚函数。
保护继承:基类的公有成员和保护成员都将成为派生类的保护成员。
当从派生类派生出另一个类时,私有继承和保护继承之间的主要区别便呈现出来了。使用私有继承时,第三代类将不能使用基类的接口;使用保护继承时,基类的公有方法在第二代中将变成受保护的,因此第三代派生类可以使用它们。
using声明:使用保护派生或私有派生时,基类的公有成员将成为保护成员或私有成员。假设要让基类的方法在派生类外面可用,可以将函数调用包装在另一个函数调用中,即使用一个using声明来指出派生类可以使用特定的基类成员,即使采用的是私有派生(只适用于继承,而不适用于包含)。使用方法如下:
class A : private std::valarray<double> { public: using std::valarray<double>::operator[];};//这将使两个版本(const和非const)都可用。
虚继承:
如果Singer类和Waiter类都继承Worker类,然后SigningWaiter类继承Singer类和Waiter类,那么SigningWaiter类将包含两个Worker组件。这将导致一些问题:
SingingWaiter d;
Worker * a = &d; //ambiguous。
Worker * a = (Waiter *) &d; //the Worker in Waiter
另一方面,真正的问题是:很多时候不应该有两个基类组件,比如这种情况下就不应该有两个基类组件。
虚基类使得从多个类(它们的基类相同)派生出来的对象只继承一个基类对象。例如通过在类声明中使用关键字virtual,可以使Worker被用作Singer和Waiter的虚基类:
class Singer : virtual public Worker {...};
class Waiter : public virtual Worker {...};
然后,可以将SIngingWaiter类定义为:class SingingWaiter : public Singer, public Waiter {...};
C++在基类是虚的时,禁止信息通过中间类自动传递给基类。因此,上面的定义会使得SigningWaiter在构造时,会构造Singer和Waiter,但是SInger和Waiter构造过程中都会省去构造Worker的过程。但是编译器在构造派生对象之前又一定要构造基类对象组件,所以它会使用默认构造函数先构造一个Worker组件出来。同时,C++也允许SingingWaiter在构造函数中显式的调用Worker的任意构造函数(对于非虚基类,这是非法的)。
同时可以大胆猜测虚继承的构造过程:编译器自底向上构造该派生类的时候,检查当前构造到的类有没有被下面的类进行虚拟继承,如果没有,则正常构造;否则就会选择出合适的构造函数,然后将该对象提供给所有虚继承了该类的中间类(也有可能是最终的派生类)使用。猜测有待验证。
混合使用虚基类和非虚基类:假设B被用作C和D的虚基类,同时被用作X和Y的非虚基类,而类M从C,D,X和Y派生而来,这种情况下,M从类C和D那共同继承了一个B,从X和Y那分别继承了一个B,所以它包含三个B类子对象。(这个和我上面的猜想是吻合的)
模板暂时没看。