1、关于变量的初始化:如果一个变量在全局内定义的,那么系统会保证给他提供初始值0。如果变量在局部内定义的,或是通过new表达式动态分配的,则系统不会向他提供初始值0。这些对象被称为未初始化的。未初始化的对象不是没有值,而是他得值是未定义的。(与它相关联的内存区中含有一个随机的位串,可能是以前使用的结果。)
因为未初始化对象是个常见的错误,而且很难发现,所以,一般建议为每个被定义的对象提供一个初始值。
2、vector有两种使用风格:
___数组风格(定义时确定大小):
vector<int> vec(10);
这与用int ia[10]相似,都可以用下表方式访问,vec[3] ia[3]
___STL风格(定义时是空的,不确定大小的):
vector<string> text;
string word;
while(cin >> word){
text.push_back(word);
}
for(vector<string>::iterator it = text.begin(); it!=text.end(); ++it){
cout << *it << ' ';
}
___不要混用两种风格:
------------------------------------------------
vector<int> vec;
定义一个空vector,再写出这样的语句:
vec[0] = 1024;
就是错误的,因为vec还么有第一个元素
------------------------------------------------
类似的,当我们定义一个给定大小的vector时,例如:
vector<int> vec(10);
任何一个插入操作都将增加vec的大小,而不是覆盖某个现在的元素。
如果执行vec.push_back(1024),那么vec的长度会增加到11,也就是说刚刚插入的元素1024是vec的第11个元素。
------------------------------------------------
3、sizeof是操作符,类似于“+=”、“&”、“++”等,而不是一个函数
int ia[] = {1.2.3};
//返回整个数组的大小,而不是数组中元素的个数
size_t array_size = sizeof ia;
//sizeof int 返回int类型的大小,下面这句返回的是数组中元素的个数
size_t element_size = array_size/sizeof(int);
但下面这种情况则是返回指针的大小,而不是数组的长度,更不是数组中元素的个数
int *pi = new int[3];
size_t pointer_size = sizeof(pi);
4、关于new、delete、vector
vector<string> svec(10);//合法
vector<string> *pvec1 = new vector<string>(10);//合法
vector<string> **pvec2 = new vector<string>[10];//非法,new返回的是指针类型(*),而不是指向指针的指针类型(**)
vector<string> *pvec3 = new vector<string>[10];//合法
vector<string> *pv1 = &svec;
vector<string> *pv2 = pvec1;
delete svec;//非法,不能delete非new的
delete pvec1;//合法
delete [] pvec2;//合法
delete [] pvec3;//合法
delete pv1;//合法
delete pv2;//合法
5、数组的长度必须是const的,但并不是所有const都能用做长度,必须是在编译期间就能确定长度的const才能使用,在运行时才确定的const类型是不能作为长度使用的;
如果想使用运行时确定长度的数组请使用:int *ip = new int[len];此时的len就可以使任意(const或非const)变量了。
不同的编译器对int ia[0]的解释不同,有正确也有错误。
vector、list等容器的长度可以使常量const,也可以是非常量。
6、string的查找返回i值:
string st("abcdabc");
string::size_type pos = find(st);
7、如果函数已经声明为inline(内联),则函数体可能已经在编译期间它的调用点就被展开。如果没有被声明为inline,则函数在运行时才被调用。
8、在C++函数传递参数时,数组永远不会按值传递。他是传递第一个元素(准确的说是第0个)的指针,下面三个声明是等价的:
void putValues(int*);
void putValues(int []);
void putValues(int [10]);
9、缺省情况下,函数的返回值是按值传递的(passed by value),这意味着得到控制权的函数将接受返回语句中指定的表达式的
拷贝。
该缺省行为可以被改变。一个函数可以被声明为返回一个指针或者一个引用。
如果返回的是一个大型对象,用引用或指针返回类型比按值类型返回类对象效率要高得多。在某些情况下,编译器自动将按值返回转换到按引用返回。该优化被称为“命名返回值优化”。
//当声明一个返回引用的函数时,当返回一个指向局部对象的引用。
//局部对象的生命周期随函数的结束而结束。
//在函数结束后,该引用变成未定义的内存的别名。
//问题:返回一个指向局部对象的引用
Matrix & add(Matrix &m1, Matrix &m2)
{
Matrix res;
.......//对m1和m2进行加操作
//返回之后,结果指向了一个有问题的位置
return res;
}
//在这种情况下,返回类型应该被声明为非引用类型。
//然后在局部对象的声明周期结束之前,拷贝局部变量。
10、extern "C" 含义:
实现C++与C及其它语言的混合编程,混合调用。例如:
在声明的时候将函数用extern "C" {xxType xxFunc();}所包围,调用的时候用extern xxType xxFunc();声明该函数后使用。
11、函数指针:
int func(int);//普通函数
int func1(int);//普通函数
int (*pf)(int);//函数指针
pf = func;//为函数指针赋值
int (*pfs[2])(int);//定义函数指针数组,长度为2
typedef int (*PFV)(int);//定义函数类型指针typedef
PFV test[2];//定义函数指针数组
test[0] = func;
test[1] = func1;
12、头文件为所向extern对象声明、函数声明、inline函数定义提供了一个集中的位置:被称作声明的局部化。如果一个文件要使用或定义一个对象或函数时,它必须包含相应的头文件。
头文件提供的声明逻辑上应该属于一个组。
头文件不应该含有非inline函数或对象的定义。符号常量(const int PI = 3.14)和inline函数不受此限制。
13、在delete指针前对指针进行测试是没有必要的,例如:
if(pi!=0)
delete pi;
//完全没有必要
但是在delete后,一定要将pi = 0。
14、定位new表达式:将对象创建在已经被分配好的内存中。形式:
new (place_address) type -specifier
使用这种new表达式,必须包含头文件<new>,这项设施允许程序员预分配大量的内存,供以后通过这种形式的new创建对象。例如:
#include <iostream>
#include <new>
const int chunk = 16;
class Foo{
public:
Foo(){_val = 0;}
int val(){return _val;}
private:
int _val;
};
//预分配内存,但没有Foo对象
char *buf = new char[sizeof(Foo) * chunk];
int main()
{
//在buf中创建一个对象Foo
Foo *pb = new (buf) Foo;
//检查对象是否被放在buf中
if(pb->val() == 0){
cout << "new expression worded!" << endl;
}
//到这里不能使用pb
delete [] buf;
return 0;
}
//执行程序输出:new expression worded!
//注意两点:1、new出来的pb不用delete;2、delete buf时注意是[];
int globalObj;
char buf2[1000];
void f(){
int *pi = &globalObj;
double *pd = 0;
float *pf = new float(0);
int *pa = new (buf2) int;
delete pi;//崩溃
delete pd;//ok,pd=0可以delete,如果声明为double *pd;delete时报错
delete pf;//ok
delete [] pa;//崩溃
}
typedef int arr[10];//定义一种类型,其包含10个整形的数组;
int *pa = new arr;
delete [] pa;//正确删除方法
15、待研究:auto_ptr、常量对象的动态分配与释放;
16、有时间回头看看第6章(抽象容器类型,尤其是他的操作文本的例子);12章泛型算法回头看看;
17、类中的非const成员变量不能在类体中显式的初始化;例如:
class FC{
public:
static int si = 9;//错误
const int si0 = 7;//正确
const static int si1 = 8;//正确
int *pi = 0;//错误
double d = 0.0;//错误
};
18、友元(friend):允许一个类授权被声明为友元(friend)的函数(类)实现中访问本类的非公有成员;例如:
友元函数:
class Foo{
friend int fu(Foo & fv);
public:
Foo():_value(15){}
private:
int _value;
};
int fu(Foo &fv)
{
return fv._value;
}
int main(void)
{
Foo *foo = new Foo();
cout << fu(*foo);
}
友元类:Query是NameQuery的基类
class Query{
freiend class NameQuery;
public:
//...
}
现在NameQuery不但可以访问它自己的基类(Query)子对象的私有成员,而且还可以访问所有Query对象的私有和被保护的成员。
19、每个类对象都有自己的类数据成员拷贝,但是,每个类成员函数的拷贝只有一份。
20、在类体中定义的函数被自动作为inline函数处理。
21、把一个成员函数声明为const可以保证这个成员函数不能修改类的数据成员,但是如果该类含有指针,那么在const成员函数中能修改指针所指向的对象。例如:
#include <cstring>
using namespace std;
class Text{
public:
void bad(const string &parm) const;
private:
char *_text;
};
void Text::bad(const string &parm) const
{
_text = parm.c_str();//错误,不能修改_text
for(int ix = 0; ix < parm.size(); ix++){
_text[ix] = parm[ix];//不好的风格,但是不是错误的
}
}
22、关于静态数据成员初始化:
class FC{
public:
static int si = 9;//错误
const int si0 = 7;//正确,int有序类型
const static int si1 = 8;//正确,int有序类型
const static char *cs = "abcd";//char* 不是有序类型
int *pi = 0;//错误
double d = 0.0;//错误
};
静态数据成员的类型可以是其所属类,而非static数据成员只能被声明为该类的对象的指针或引用。如:
class Bar{
private:
static Bar bar;//ok
Bar *pbar;//ok
Bar &rbar;//ok
Bar bar0;//错误
};
23、指向类成员函数的指针:
#include <iostream>
using namespace std;
int (*fp)();
int (*fpstatic)();
class Screen{
public:
inline int high(){return _high;}
static int statichigh(){return 90;}
private:
int _high;
};
int main(){
fp = &Screen::high;//非法赋值,类型违例
fpstatic = &Screen::statichigh;//静态类型的合法,与普通函数指针相同
}
//非static成员函数的指针必须与其赋值的函数类型匹配在下列三个方面都匹配:
//1)参数的类型和个数;2)返回类型;3)它所属的类类型
//所以fp必须被声明为如下形式,保证fp的类类型为Screen
int (Screen::*fp)();
//才能使用
fp = &Screen::high;
24、联合、位域、类域、嵌套类、局部类有时间看看。
25、类的初始化、构造函数、析构函数相关:
a、显式初始化表:通过显式初始化表,用常量初始化大型数据结构比较有效。例如,我们创建一个调色板,或者向一个程序文本中注入大量的常量值,如一个复杂地理模型中的控制点和节点值。在这种情况下,显式初始化可以在装载时刻完成,从而节省了构造函数的启动开销(即使它被定义为inline),尤其是对全局对象。
class Data{
public:
int ival;
char *ptr;
};
//Date类需要提供一个构造函数嘛?正如它的定义所示,它不需要,因为它的所有数据成员都是公有的。从C语言继承来的机制支持显示初始化表,类似于用在初始化数组上的初始化表。例如:
int main()
{
//local1.ival = 0; local1.ptr = 0;
Data local1 = {0, 0};
//local2.ival = 1024;local2.ptr = "Look at"
Data local2 = {1024, "Look at"};
}
//根据数据成员被声明的顺序,这些值按位置被解析。例如下面是一个编译错误,因为ival在ptr之前被声明:
Data local3 = {"Look at", 1024};
b、不指定参数来定义对象:只有当没有构造函数或声明了缺省构造函数时,我们才能不指定实参来定义类对象。一旦一个类声明了一个或多个构造函数,类对象就不能被定义为不调用任何构造函数的实例。尤其是,如果声明了一个包含多个参数的构造函数,但没有声明缺省构造函数,则每个类对象定义的时候都必须提供所需的参数。
在实践中,如果定义了其他构造函数,则也有必要提供一个缺省构造函数。如果没有缺省构造函数,下面的语句就是错误的:
Account *pact = new Account[1024];//在new的过程中会调用缺省构造函数,如果Account没有提供缺省构造函数则是错误的。
同样情况,容器类(比如vector)要求他们的类元素或者提供缺省的构造函数,或者不提供构造函数。
不指定参数类定义对象,要注意第四点!!!
@explicit 只对构造函数起作用,用来抑制隐式转换。
c、成员初始化表:成员初始化表只能在构造函数定义中被指定,而不是在其声明中。该初始化表被放在参数表和构造函数体之间,由冒号开始。
inline Account::Account()
:_name(0),
_balance(0),_acct_nmbr(0)
{}
一个const类对象在“从构造函数完成到析构函数开始”这段时间内才被认为是const的,volatile类对象一样。
d、析构函数:
destructor函数被调用于函数的每一个退出点,以便处理每一个local class object,如果destructor是个inline destructor,函数的每一个退出点都会 被编译器安插一份destructor程序代码(可能造成程序代码膨胀)。
e、类构造函数执行过程:
@在执行构造函数体中的代码之前,首先会依次按类中声明的成员变量顺序调用各个成员变量的默认构造函数(如果有的话,如:内置数据和指针类型不初始化)或者初始化参数列表中要求的构造函数,然后才进入构造函数执行。
@初始化阶段可以是显式的或是隐式,取决于是否存在成员初始化表。隐式初始化阶段按照声明的顺序依次调用所有基类的缺省构造函数。然后是所有成员类对象的缺省构造函数。
@如果成员是类对象,则初始化表初始化此成员变成传递给适当的构造函数的参数,该构造函数然后被应用到成员类对象上!!
@用一个成员对另一个成员进行初始化(如果你真的认为很必要)的代码放在构造函数体内。
@下列三种情况需要使用初始化表:
@内置类型不需要必须,但最好也放在初始化列表中。
@查看一下例子:
class B{
public:
B()
{
cout << "B constructor." << endl;
}
};
class Query{
public:
Query()
:_solution(0)
{
cout << "Query constructor." << endl;
}
protected:
set<short> *_solution;
vector<B> _vb;
};
int main()
{
/*执行Query构造函数,先根据初始化表初始化_solution为0,然后缺省的vector构造函数被自动调用。
注意此过程中和类B没有任何关系,虽然vector中包含的是B类,因为此时的_vb是空的,只是声明而已。
但请注意:如果Query初始化表中填入“_vb(3)”,则会调用vector的带参构造函数vector(int),代表
初始化3个B类,此时B类的默认构造函数B()会被调用3次*/
Query q;
return 0;
}
f、经过一番错误尝试与探索之后,我们发现,如果class拥有至少一个指针,那么它就需要一个copy constructor。
g、按成员初始化(如果程序员不提供copy constructor,编译器便提供一份memberwise初始化操作):
h、按成员赋值:
26、C++支持多态的三种形式:
Query为基类,NameQuery为其派生类,eval()是Query的虚拟函数;
@通过一个隐式转换,从“派生类指针或引用”转换到“其公有基类类型的指针或引用”
Query *pquery = new NameQuery("Glass");
@通过虚拟函数机制:
pquery->eval();
@通过dynamic_cast和typeid操作符:
NameQuery *pnq = dynamic_cast<NameQuery*>(pquery);
27、继承关系中,构造函数调用顺序总是如下:
@基类构造函数。如果有多个基类,则构造函数的调用顺序是某类在派生表中出现的顺序,而不是他们在成员初始化表中的顺序。
@成员类对象构造函数。如果有多个成员类对象,则构造函数的调用顺序是在类中被声明的顺序,而不是他们出现在成员初始化表中的顺序。
@派生类构造函数。
28、派生类并不继承基类的构造函数!!每个派生类都必须提供自己的构造函数集;可以在派生类构造函数的成员初始化表中初始化基类构造函数;
(派生类是不会继承基类的构造函数和析构函数的,这话是对的。但是不继承,不代表不调用,正因为不继承,所以才需要调用基类的构造和析构函数来构造自己的构造和析构函数。注意:派生和调用是两个行为;)
class Base{
public:
Base(int, double *):ip(0){}
private:
int *ip;
};
class Derived : public Base{
public:
Derived(int i, double * pd):cp(0), Base(i, pd){}//先执行Base构造函数,再初始化cp
private:
char *cp;
};
@另外,派生类构造函数只能合法调用其直接基类构造函数(虚函数为这条规则提供了一个特例);也就是说派生类不能调用他爷爷级别基类以上基类的构造函数。
@派生类不能在成员初始化表中初始化其基类的成员变量;
@已知下面基类定义
class Base{
public:
explicit Base(int);
virtual ostream& print(ostream &);
virtual ~Base();
static int object_count();
protected:
int _id;
static int _object_count;
};
@下列代码错误在哪里
/
class C1:public Base{
public:
C1(int val)
:_id(_object_count++){}
};
//做修改,因为_id是Base的成员变量,初始化_id是Base的事情
class C1:public Base{
public:
C1(int val)
:Base(val){}
};
///
/
class C2:public Base{
public:
C2(int val)
:Base(_id+val){}
};
//没有编译错误,Base的调用是个危险的操作,因为它使用_id,而_id当时尚未初始化!!
///
29、虚函数:
@缺省情况下,类的成员函数是非虚拟(novirtual)的。当一个成员函数为非虚拟的时候,通过一个类对象(指针或引用)而被调用的该成员函数,就是该类对象的静态类型中定义的成员函数。
@只有通过基类指针或引用间接指向派生子类型时,多态才会起作用。使用基类对象并不会保留派生类的类型身份。
class Base{
public:
virtual void foo();
};
void Base::foo()
{
cout << "Base foo." << endl;
}
class Drived : public Base{
public:
void foo();
};
void Drived::foo()
{
cout << "Drived foo." << endl;
}
void print(Base b, Base *pb, Base &rb)
{
b.foo();//out:Base foo.
pb->foo();//out:Drived foo.
rb.foo();//out:Drived foo.
}
int main()
{
Drived d, d0;
Drived *pd = new Drived;
Drived &rd = d0;
print(d, pd, rd);
return 0;
}
@引入虚函数的类必须定义(实现)它,或者把它声明为纯虚函数!如果派生类可以提供自己的实例,那么此实例将成为该派生类的活动实例,或各派生类也可以继承基类的活动实例。如果派生类定义了实例,则称之为改写(override,覆盖)了基类的实例。
@为了使虚函数的派生类实例能够改写基类的活动实例,它(派生类)的原型(函数原型)必须和基类完全匹配。但有一个特例,派生类实例的返回值可以是基类实例返回值类型的公有派生类型,用在clone()上很有用处。
30、纯虚函数:
@一个虚函数只提供被子类型改写的接口,但是,它本身并不能通过虚拟机制被调用。
@包含(或继承)一个或多个纯虚函数的类被编译器识别为抽象基类。试图创建一个抽象基类的独立类对象会导致编译时刻错误。(类似的,通过虚拟机制调用纯虚函数也是错误的)。
@抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类没有重新定义纯虚函数,而派生类只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体类了
31、虚拟函数的静态调用:
class Base{
public:
virtual void func();
};
void Base::func()
{
cout << "Base func." << endl;
}
class Drived : public Base{
public:
void func();
};
void Drived::func()
{
cout << "Drived func." << endl;
}
int main()
{
Base *d = new Drived;
d->func();//虚拟机制动态调用
d->Base::func();//在编译时刻静态调用
return 0;
}
32、虚拟函数和缺省实参:
class Base{
public:
virtual int foo(int ival = 1024);
};
int Base::foo(int ival)
{
cout << "Base foo--ival=" << ival << endl;
return ival;
}
class Drived : public Base{
public:
int foo(int ival = 2048);
};
int Drived::foo(int ival)
{
cout << "Drived foo--ival="<< ival << endl;
return ival;
}
int main()
{
Drived *pd = new Drived;
Base *pb = pd;
int val = pb->foo();
cout << "main():pb return=" << val << endl;
val = pd->foo();
cout << "main():pd return=" << val << endl;
return 0;
}
/*程序输出:
Drived foo--ival=1024
main():pb return=1024
Drived foo--ival=2048
main():pd return=2048
注意!!!如果去掉Base::foo中的virtual,程序输出如下:
Base foo--ival=1024----------这里不一样!!!!没有virtual就静态调用
main():pb return=1024
Drived foo--ival=2048
main():pd return=2048
*/
在这两个调用中,foo()派生类实例被正确调用。这是因为,foo()调用的真正实例是在运行时刻根据pd和pb指向的实际对象类型决定的。然而,传递给foo的缺省实参不是在运行时刻决定的,而是在编译时刻根据被调用函数的对象的类型决定的。当通过pb调用foo()时,缺省实参中base::foo()的声明决定,为1024。当通过pd调用foo()时,缺省实参用derived::foo()的声明决定,为2048。注意如果去掉Base::foo中的virtual,则不存在虚拟函数调用机制,pb调用foo()时依然是静态函数调用,调用Base的函数foo(),自然参数也就是默认的1024。
33、虚拟函数的析构函数:
一般而言,建议将class层次结构中root base class(声明一个或多个虚拟函数者)的destructor声明为virtual(原因很简单,如果基类指针指向的是派生类对象,那么通过delete pBase;的时候,只会调用基类的析构函数,只有基类部分的资源被释放,造成内存泄漏。但是如果是虚析构函数,那么delete pBase;会调用派生类的析构函数,派生类的析构函数中释放掉派生类部分的资源后会自动调用基类的析构函数,才能把整个派生类对象资源释放掉)。当对例如:
class Base{
public:
virtual ~Base();
virtual void doit() = 0;
}
class Location{
public:
/*...*/
int x;
int y;
};
class Query{
public:
/*...*/
private:
int _paren;
set<short> *_solution;
vector<Location> _loc;
};
class NameQuery : public Query{
public:
/*...*/
private:
string _name;
};
int main()
{
NameQuery folk("folk");
NameQuery music = folk;
}
/*
main函数中的两句会导致如下事情发生:
1、编译器检查NameQuery是否定义了一个显式的拷贝构造函数实例。答案是没有。所以,编译器准备应用缺省的按成员初始化。
2、编译器接下来检查NameQuery基类子对象。是的,它含有Query基类子对象。
3、编译器检查Query基类是否定义了显式的拷贝构造函数实例。答案也是没有。所以编译器准备应用缺省的按成员初始化。
4、编译器检查Query是否含有基类子对象。没有。
5、编译器以声明的顺序检查Query的每个非静态成员。如果成员是非类对象,如_paren和_solution,则它用folk的成员值初始化music
对象的成员。如果成员是类对象,如,_loc,则它递归应用步骤1。是的,vector类定义了一个显式的拷贝构造函数实例。该拷贝构造函数
被调用,用flok._loc初始化music._loc。
6、然后编译器按声明的顺序检查NameQuery类型的每个非静态成员。string成员类对象被识别出来,它有一个显式的拷贝构造函数。于是
调用该拷贝构造函数,用flok._name初始化music._name
*/
35、当我们无法为base class 增加虚拟函数时,我们会使用dynamic_cast。