以下总结来自于
《标准C++程序设计教程》林丽闽,褚尚军等编著
《标准C++开发入门与编程实践》白乔编著
1. 指针和数组
int aa[10]={1,2,3,4,5,6,7,8,9,10};
int *dp1 = &aa[2];
int *dp2 = aa+2;
cout<<"*dp1=:"<<*dp1<<" *dp2=:"<<*dp2<<endl;
结果均会输出*dp2=3, *dp2=3.
2. Sizeof 用法
int *i_point;
int i;
sizeof i; //为4
sizeof (int); //为4
sizeof i_point; //为4
sizeof &i; //为4
3. 引用只是一个别名而已, 注意只是一个别名而已. 引用即不是对原对象的复制, 也不是指向原对象的指针, 实际上, 编译器把它作为原对象的另外一个名字.
C++中的引用是其它变量的别名. 声明一个引用型变量, 需要给他一个初始化值, 在变量的生存周期内, 该值不可改变.
用法:
int a;
int &refera = a;
int b;
refera = &b; //不能再进行重新赋值. 编译报错: cannot convert from 'int *' to 'int'
&refera =b; //编译报错:cannot convert from 'int' to 'int *'
3.1 以引用作为函数参数, 见经典的swap(date &dt1, date &dt2){ date save = dt1; dt1=dt2; dt2 = save;}, 调用时用swap(dt1, dt2). 可以避免用指针的复杂操作.
若用指针, 则应swap(date *dt1, date *dt2){date save = *dt1; *dt1= *dt2; *dt2 = save;}, 调用时用swap(&dt1, &dt2); 调用时较为麻烦.
3.2 以引用作为返回值, int data[]={1,2,3,4,5}; 进行数据的调用int &getdata(int n){return data[n-1];}
4. Const用法总结
4.1 设置变量为const, 例: const int a =23;
4.2 函数返回类型为const, 即说明这个函数返回的值为const类型,主要是为了对返回值进行保护。例:int data[]={1,2,4,5,6};函数定义:const int& getdata(int n){return data[n-1];} 。进行数据的调用:const int &value = getdata(2);
4.3 函数的定义中,函数名后加const, 表示这个函数不会修改当前类中的数据。 通常是在类方法中进行的,例:
class Person
{
private:
string name;
int age;
public:
Person(string _name, int _age):name(_name),age(_age){}
void ShowPerson() const //此处进行了修饰,如果在这个方法中有修改当前类的数据成员操作,则会编译不通过
{
cout<<"Name="<<name<<", Age="<<age<<endl;
}
};
4.4 Const int* 与int *转换
int i =23;
int *p = &i;
int const *pp=p; //说明可以把int *转换成const int*型
*pp =2344; //修改失败, 因为pp是常量指针
*p =233; //可以正常修改
int *ppp =pp; //赋值失败, 因为不能把const int*转换成int *型
5. 类的构造函数,默认构造函数,默认转换构造函数,成员转换函数。
有两种转换函数:一种是转换构造函数,另一种是成员转换函数。需要采用哪种转换函数取决于转换的方向。
5.1 默认构造函数,
例:
class Person
{
private:
string name;
int age;
public:
Person(string _name="Jacky", int _age=27):name(_name),age(_age){} //带默认值的默认构造函数
};
5.2 默认拷贝构造函数
例:
class Person
{
public:
string name;
int age;
Person(string _name, int _age):name(_name),age(_age){}
};
用法: Person Jacky(“JACKY”,21); Person Tom = Jacky; Person Jonh(Jacky); 都会进行默认的拷贝构造, 即全部成员进行拷贝, 从而会引出浅拷贝和深拷贝的问题, 以后再做讨论.
5.3 自定义拷贝构造函数
例:
Person(const Person &object)
{
cout<<"Hello, I'm called!"<<endl;
name = object.name;
age = object.age+10;
}
用法: Person Jacky(“JACKY”,21); Person Tom = Jacky; Person Jonh(Jacky); 都会进行这个自定义的拷贝构造函数调用. 但是Person Tom = Jacky其实是因为大部分编译器进行优化成Person Tom(Jacky).
5.4 转换构造函数, 当一个构造函数仅有一个参数, 且该参数是不同于该类的一个数据类型时,这样的构造函数就叫做构造函数。
例:
class Person
{
private:
string name;
int age;
public:
Person(string _name, int _age):name(_name),age(_age){}
Person(string jj){name =jj; age =20;} //这个为转换构造函数
};
1.调用时用可Person pp(“Jacky”). 此时就相当于一个普通的构造函数进行调用
2.调用时也可用Person pp=”Jacky”. 这种用法有时并不是期望的, 因为我们可能只是用的是普通的构造函数而已, 为了避免出现这种调用, 可以用 explicit Person(string jj){ name =jj; age =20;}, 则再进行Prerson PP=”Jacky” w会出错
5.5 类型转换函数,它可以把该类的对象转换为其它数据类型的对象.用到C++的关健字operator, 例:Classname :: operator long(){}
例:
class Person
{
private:
string name;
int age;
public:
Person(string _name="Jacky", int _age=27):name(_name),age(_age){}
Person(string jj){name =jj; age =20;}
operator int(){ cout<<"I'm called"<<endl; return age;} //类型转换函数
};
类型转换函数调用方法:
Person x="JJJJ";
int kk=x; //调用类型转换函数
cout<<kk<<endl; //输出20
5.6 转换运算符的调用优先顺序.
例: Person p1(“JJ”), Person p2(“KK”), p3;
若有:p3 = p1 + p2;
系统会进行如下的查找顺序:
1. 寻找成员函数的+运算符; 如: Person Person::operator + (Person & s){}//其中可以使用this指针.
2. 寻找非成员函数的+运算符; 如: Person operator +(Person &s1, Person &s2){}
3. 寻找存在的内部运算符;如:operator +(double, double). 或者operator +(int,int)
4. 寻找能将实参Person对象转换成double型的转换运算符operator doule(). 或者int.
6. typid操作符, 用以返回一个变量或数据类型的类型.
用法如下:
1.可以判断两个类是否相等,
例:
bool b;
b = (typeid(100) == typeid(200)); //b =true;
b = (typeid(100) ==typeid(“100”)); //b =false;
b = (typeid(100) != typeid(“100”)); //b =true
2.typeid(类型), cout<<typeid(int).name()<<endl; 输出:int
3.typeid(变量), int a; cout<<typeid(a).name()<<endl; 输出:int
4.综合sizeof和typeid使用, 可以了解到更多的C++内幕.
如:cout<<typeid(cout).name()<<endl; 输出:
class std::basic_ostream<char,struct std::char_traits<char> >
解释:cout是std名空间下的basic_ostream模板的某个实现类.
再如: cout<<typeid(sizeof(int)).name()<<endl; //输出sizeof返回值size_t的类型名称
输出: cout<<typeid(sizeof(int)).name()<<endl;
解释: sizeof的返回值, 在大部分情况下是一个unsigned int.
7. Ref的用法与指针、数组对比
例1:主要是说明用引用可以进行修改
int data[]={1,5,8,65,66,77,48,89,130}
int &ref =data[0];
cout<<ref++<<endl; //输出1
cout<<ref++<<endl; //输出2
cout<<ref++<<endl; //输出3, //最后data[0]已经被改成4.
例2:不能定义引用的数组
int i;
int &ri[2]={i,i} //引用数组非法
int&rb[2]={1,2} //引用数组非法
8. 位域, 初衷是为了节约内存
用法如下:
struct Compactime
{
unsigned hour:5; //5 bits, 0-31
unsigned minute:6; //6 bits, 0-63
unsigned second:6; //6 bits, 0-63
};
9. 可变参数用法
#include <cstdarg>
int add(int first...)
{
//准备读取可变参数
va_list nums;
va_start(nums, first); //使用<cstdarg>中的va_xxx宏参数处理可变参数
int sum=0;
int num = first;
//依次读取参数,-1表示结束
while (num!=-1)
{
cout<<"+"<<num<<",";
sum+=num;
num=va_arg(nums,int);
}
va_end(nums);
return sum;
}
调用之
int sum=add(1,2,3,-1); //分别输出: +1,+2,+3
cout<<sum<<endl; //输出:6
10. 重载函数名字机制, 名字重组, 即name mangling. 而在使用其它语言生成的模块, 其它语言生成的名字, 通常不会和当前的名字重组方法相同, 所以需要用以标注来说明不对其它语言的函数进行名字重组. 所以引入了extern “C” int add1();来解决.
11. 宏参数具有一个明显的优势, 那就是它避免了C++的强类型检测, 当然也可以隐藏错误,直到最后运行时才可能被发现的错误.
12. 内联成员函数, 内联函数可以在编译时期被适时展开, 从而避免由于函数栈的展开而消耗更多的时间. 例如: inline void Actor:: setName(string name, bool gender){_name = name, _gender=gender;}
实际上, 在类体内定认的成员函数,会自动处理成inline. (但要注意的是,只限于在类体定认的成员函数,即成员函数体在类定义之中)
13. 成员函数实际上并不属于对象, 它只是一个特别的全局函数.
如Person Jacky; Jacky.Show(); 它很可能被编译器处理成global_Point_Show(&Jacky); 这里global_Point_Show()对应于某个全局函数. 这也是为什么我们在做debug时,看不到对象的方法,而只能看到对象的子成员.
class Person
{
private:
string name;
int age;
public:
Person(string _name, int _age):name(_name),age(_age){}
void ShowPerson() const
{
cout<<"Name="<<name<<", Age="<<age<<endl;
}
};
使用Person tester(“Jacky”,20); 观察tester对象
同时也说明了, 对于Show()函数,即便它是private型的,我们同样可以通过指针的方式进行调用, 在virtual table文章中,我提到了如何通过指针去访问private类型的方法.
14. Volatile类型个修饰符, volatile型变量和const型变量恰恰相反, volatile类饰符通知编译器, 程序中将以某些不可见的方式修改变量的值.至于到底如何改变, 取决于具体实现,一种典型的可能是变量在异步中断服务例程中被修改.编译器必须事先知道变量可能被修改,否则就会因为优化了对变量的访问而阻止外部对变量的改动.
用法如下: volatile int value = 32;
15. Mutable修饰符, 主要是应对
string Person::toString() const //注意其中的const.
{
if(name ="")
{
name = "No name";
}
return name;
} //这会导致编译不通过, 因为对成员变量进行了修改, 还谈什么constr呢?
一种方法是放弃使用const, 但这样做实在是下下策, toString()嘛, 听起来就应该是一个const函数, 于是, C++提供了mutuable关健字, 为以上的冲突提供了一个后门.
只需要在类定义中把name的定义改成mutuable string name; 即可.
16. 友元使用.
16.1 声明某个函数为友元
例:
class Person
{
private:
string name;
int age;
public:
Person(string _name="Jacky", int _age=27):name(_name),age(_age){}
friend void Family(Person p1, Person p2);
void ShowPerson() const
{
//Person tester("Jacky");
//this = 0;
cout<<"Name="<<name<<", Age="<<age<<endl;
}
};
void Family(Person p1, Person p2)
{
cout<<p1.name<<" + "<<p2.name<<endl; //注意这里的name是Person类的私有变量, 如果不是友元,那么这里是无法访问到私有变量的.
}
16.2 声明某个类的成员函数为友元
例:
class Person; //声明
class Teacher
{
public:
void His_Teacher(Person p); //声明
};
class Person
{
private:
string name;
int age;
public:
Person(string _name="Jacky", int _age=27):name(_name),age(_age){}
friend void Teacher::His_Teacher(Person p); //类的成员函数为友元
void ShowPerson() const
{
cout<<"Name="<<name<<", Age="<<age<<endl;
}
};
void Teacher:: His_Teacher(Person p){cout<<p.name<<endl;}
16.3 声明某个类为友元
例:
class Person
{
private:
string name;
int age;
public:
Person(string _name="Jacky", int _age=27):name(_name),age(_age){}
friend class Teacher; //声明类为友元
void ShowPerson() const
{
cout<<"Name="<<name<<", Age="<<age<<endl;
}
};
class Teacher
{
public:
void His_Teacher(Person p){cout<<p.name<<endl;};
};
17. 继承
例: class Actor
{
private:
string _name;
bool _gender;
public:
Actor(string name, bool gender):_name(name), _gender(gender){} //自定义构造函数
Actor(){} //默认构造函数
};
class Monk:public Actor
{
private:
string _monkName;
public:
Monk(string name, string monkName):Actor(name,true),_monkName(monkName){} //自定义构造函数
};
说明:在Monk自定义构造函数中,为了初始化Actor中的变量, 所以采用:Actor(name, true). 如果在Actor类中即无自定义构造函数,也没有显示的默认构造函数, 则会编译失败, 说基类没有Actor(string, bool)构造函数定义.
如果基类没有定义任何自定义构造函数, 也没有显示定义默认构造函数, 则Monk(string name, string monkName):_monkName(monkName){}可行, 实例化时会自动调用基类的默认构造函数.
18. 隐藏和覆盖
隐藏是指派生类与基类有相同的函数名(只要求名称相同即是隐藏,不要求其参数相同或返回值相同)或者变量名, 并且基类没有用virtual进行声明, 这个时候可以用::域操作符进行指定调用基类的函数或变量名.
如: class Actor
{
public:
void say(){cout<<"In Actor"<<endl;}
};
class Monk:public Actor
{
public:
void say(string name){cout<<"in Monk"+name<<endl;} //隐藏了基类的同名函数.
};
覆盖则与virtual相关, 例:
class Actor
{
public:
virtual void say(){cout<<"In Actor"<<endl;}
};
class Monk:public Actor
{
public:
void say(){cout<<"in Monk"<<endl;}
};
int main(int argc, char* argv[])
{
Monk m;
Actor *ap = &m;
Monk *mp=&m;
ap->say(); //显示In Monk. 如果没有Virtual声明, 则此处显示In Actor.
mp->say(); // 显示 In Monk.
return 0;
}
C++在处理对象的指针和引用变量时, 如ap->say(), 会根据具体情况判断.
19. 虚析构函数. 如果基类的析构函数不是虚函数,针对基类指针的析构操作将无法引起派生类的析构.
例: class Actor
{
public:
Actor(){cout<<"Generate in Actor"<<endl;}
~Actor(){cout<<"Destructor in Actor"<<endl;}
};
class Monk:public Actor
{
public:
Monk(){cout<<"Generate in Monk"<<endl;}
~Monk(){cout<<"Destructor in Monk"<<endl;}
};
int main(int argc, char* argv[])
{
Actor *ap=new Monk();
delete ap;
return 0;
}
输出:
Generate in Actor
Generate in Monk
Destructor in Actor
可以知道, 析构指向Actor的指针不会引起派生类析构函数的调用,当前例子不会影响到系统内存动态分配问题,但是如果Monk的析构中牵涉到内存动态分配的情况下,忽略了对派生类析构函数的调用,可能会带来致命的错误.
解决办法是在基类的析构函数前加入virtual, 即virtual ~Actor(){cout<<"Destructor in Actor"<<endl;}
再运行结果为:
Generate in Actor
Generate in Monk
Destructor in Monk
Destructor in Actor
20. 纯虚函数,抽象类
纯虚函数是一种特殊的虚函数, 它没有函数的实现, 例: virtual void say()=0;
带有纯虚函数的类即为抽象类, 抽象类不能被实例化.
21. 多继承中的构造顺序.
多继承中基类的构造函数被调用的顺序取决于其在基类列表中声明的顺序, 而不是构造函数的初始值顺序. 析构顺序与此相反.
构造对象和基类构造函数的优先调用顺序
1. 虚拟基类的构造函数按它们被继承的顺序构造.
2. 非虚拟基类的构造函数按它们被继承的顺序构造.
3. 成员对象的构造函数按它们声明的顺序调用.
4. 最后才是类自己的构造函数.
class Actor
{
public:
Actor(){cout<<"Generate in Actor"<<endl;}
~Actor(){cout<<"Destructor in Actor"<<endl;}
};
class Terst
{
public:
Terst(){cout<<"generate in Terst"<<endl;}
~Terst(){cout<<"Destructor in Terst"<<endl;}
};
class Monk:public Actor, public Terst //基类列表中声明的顺序.
{
public:
Monk(): Terst(), Actor(){cout<<"Generate in Monk"<<endl;}
~Monk(){cout<<"Destructor in Monk"<<endl;}
};
int main(int argc, char* argv[])
{
Monk x;
return 0;
}
输出:
Generate in Actor
generate in Terst
Generate in Monk
Destructor in Monk
Destructor in Terst
Destructor in Actor
22. 虚拟继承,
1. 如果两个父类具有相同的方法, 那么子类调用这个方法时,编译器不会知道该调用哪个方法, 然后就出错.
class Actor
{
public:
void say(){cout<<"In Actor"<<endl;}
};
class Terst
{
public:
void say(){cout<<"In Terst"<<endl;}
};
class Monk:public Actor, public Terst
{
public:
};
int main(int argc, char* argv[])
{
Monk x;
x.say();
return 0;
}
所以为了解决这个问题, 只能是用x.Terst::say(), 或者x.Actor::say()
2. 对于两个或两个以上的父类当中具有相同的变量名, 则会引起派生类中的二义性和内存冗余问题.
例:
Class Actor{}
class Monk: Public virtual Actor{}
class God: public virtual Actor{}
class Buddha: public Monk, public God
{
Public:
Buddha():Actor(), Monk(), God() //这里要注明Actor()在前. 如果不进行显示对Actor()的继承, 则会使用已有的Actor中的公共部分.
{
//构造函数
}
}
尽管Monk和God都含Actor, 但是C++能保证在Buddha中只会保持一份Actor, Buddha在虚拟继承中被称作最终派生类, 那么在最终派生类中Actor的那一份唯一的复制何时进行初始化呢?如何进行初始化? C++会在Buddha的构造时首先构造Actor. 如果Actor没有无参数的构造函数,Buddha则必须在构造函数的初始值列表中显示的进行初始化
23. 定制服terminate()的行为
可以认为terminate()函数使用一个全局的函数指针进行调用:
Set_terminate(terminate_handler handler){}
通常,每个C++编译器都可能有自己的实现方式.
24. 对抛出异常的声明
Bool Save()throw(KilledException)
{
}
也可以在括号中列举出所有可能的异常类型.C++中,对抛出异常作出声明并不是强制性的.
1.如果一个函数可能会抛出异常,并不一定非要使用throw语句来抛出异常. 如申请堆空间失败.
2.反之,如果一个函数永远不会抛出异常,也可以为它声明为可能抛出异常.
3.声明的异常不必与实际抛出的异常类型完全吻合, 它们可以不一样.
4.在catch语句中,其捕获的异常不必与函数声明的异常中的类型吻合
5.如果抛出了没有在异常列表中的异常,则会激发unexpected()的调用, unexpected()默认的行为是直接调用系统的terminate().当然这里也可以重新定义terminate()的行为,而不是默认的crash行为.
25. 抛异常
抛异常与异常处理程序之间,是按数据类型的严格匹配来捕获的, 所以catch(unsigned int){…} 是不能捕获到throw 20; 的异常的.
26. 类型转换用法
此例中Monster继承自Actor.
所以
Monser m;
Actor *ap = &m; //这是一个向上的转换不会有什么异常发生,可以正常运行.
但是如果用
Actor a;
Actor *ap = &a;
Monster *mp = (Monster *)ap; //这是一个向下的转换,编译时不会出现错误,运行时如果把指针指向的对象当成Monster的对象,则可是极度危险的,而且程序员很难发现这个错误.所以用到动态类型转换符来规避这个错误
1. 动态类型转换符dynamic_cast
例1:出现非法转换时,指针的值会被赋成0
Monster *mp = dynamic_cast<Monster *>(ap); // 针对指针的强转换
If(mp != 0)
{
//正常转换了,此时发现这个指针指向的对象就是一个Monster对象
}
Else
{
//此时指针返回为NULL, 发现指针指向的对象不是一个Monster对象.
}
例2:出现非法引用转换时,会抛出bad_cast异常来通知这个错误.
Actor a;
Try
{
Monster &mr = dynamic_cast<Monster &>(a); //针对引用的强转换
}
Catch(bad_cast bc)
{
Cout<<这个不是Monster的引用类型<<endl;
}
注意:与typeid一样,动态转换不能应用于非虚继承的派生类上.即基类与派生类必须有多态的发生.
class A
{
public:
virtual void say(){} //必须有多态的发生,否则dynamic_cast会因为Class A不具备多态而失败.
};
class B: public A
{
public:
void say(){}
};
int main(int argc, char* argv[])
{
A a;
B *ap = dynamic_cast<B*>(&a);
return 0;
}
2. 静态类型转换符static_cast
Monster和God类都继承于Actor类,
God g_object;
Actor *ap = static_cast<Actor*>(&g_object); //OK, safe
Monster *mp = static_cast<Monster*>(ap); //OK, unsafe
God *gp = static_cast<God *>(mp); //Error, safe. //这个原因是static_cast不能完成2个不相干的类对象之间的转换.
3. 再解释类型转换符reinterpret_cast, 是用来做一些不可思议的转换.实际上,所有指针的值都是一个表示地址的数值,值本身的转换是没有任何问题的. 所谓"再解释",是指对指针的类型进行重新解释.再解释类型转换reinterpret_cast完成不同类型指针之间的相互转换,同时也支持将指针与数字之间的转换.
如:God *gp =reinterpret_cast<God*>(mp); //OK, safe.
4. 常类型转换符const_cast, 主要是用来修改类型的const或volatile属性. 除了const 或volatile修饰外, 其操作对象的类型和目标类型必须是一样的.
例1:
int const &i = 100;
int &m = const_cast<int &>(i);
m = 200;
cout<<i<<endl; //输出200
例2:
int const a = 100;
int const &i= a;
int &m = const_cast<int &>(i);
m = 400;
则i =400; a = 400;