函数返回值时,要生成一个值的副本。而用引用返回值时,不生成值的副本
Float temp;
Float fn1(){
Reurnntemp;
}
Float & fn2(){
Reurntemp; //注意返回引用的用法
}
Int main(void)
{
Floaa = fn1(5.0); //1
Float&b = fn1(5.0); //2 warning
Floatc = fn2(5.0); //3
Float&d = (5.0); //4
}
1中,C++会在栈中创建一个临时变量并将temp的值复制给该变量。然后在主函数中,a=fn1(5.0)再把临时变量复制给a
2中,同样会在栈中生成一个临时变量,再在主函数中用该临时变量初始化引用。根据C++标准,该临时变量会在b=fn1(5.0);后消失,所以b引用的值是无法确定的。BC对C++的标准做了扩展,规定临时变量与引用的生命周期一样。所以该语句依赖编译器,可移植性差。
3中,fn2()返回一个引用,不会产生临时副本。在main中直接把temp的值直接初始化引用c
4中,fn2()返回一个引用,直接用该引用来初始化d,且d为全局变量temp的别名。Temp生命周期会在整个程序运行期间。如果temp为全局变量就有问题了。
只要避免将局部就是的地址返回,就能使函数调用表达式作为左值来使用。这样就可以直接对返回进行操作
Lenvl(array[i], 10, typeA, tpyeB)++;
Int & level(int grade[], int size, in&tA, int &tb){
If(sum> 80)
ReurntA;
Else
ReturntB;
}
一.Const限制
a) 没有区分const引用和const变量引用,因为变量的指向本来就是const的。
没有const double cons &a = 1;,只有const double &a = 1;
二.函数
l 内联函数
函数的调用需要建立栈内存环境,传参。保护上下文等,需要开销。对于一个短且调用频率高的函数会消耗太多资源。应该使用内联函数。以牺牲代码空间来节省函数调用的开销,内联函数使用不当就会造成代码膨胀
Inline intisnumber(char ); //声明与定义必须配套,否则当普通函数处理
Inline int isnumber(charc){
}
与宏相似,但有函数的样子,可以解决宏的二义性。还可以函数成员变量
注意事项:1.在内联函数内不允许用循环语句和开关语句。 如果内联函数有这些语句,则编译将该函数视同普通函数那样产生函数调用代码,递归函数(自己调用自己的函数)是不能被用来做内联函数的。内联函数只适合于只有1~5行的小函数。对一个含有许多语句的大函数,函数调用和返回的开销相对来说微不足道,所以也没有必要用内联函数实现。 2.内联函数的定义必须出现在内联函数第一次被调用之前。 3.本栏目讲到的类结构中所有在类说明内部定义的函数是内联函数。
l 重载函数
对于在不同类型上作不同运算而又用同样的名字的情况,被视为重载。
a) 调用方法:将实参类型和所有被调用的函数的形参类型一一比较。顺序为:
(1) 寻找一个严格的匹配,如果找到了,就用那个函数
(2) 通过内部转换寻求一个匹配,只要找到了,就用这个函数
(3) 通过用户定义的转换寻求一个匹配,若能查出一个唯一的一组转换,就用那个函数
C++允许int到long, int到double的转换。当实参是整数,而重载函数为一long形参数,一为double型参数时,应该给以一个显式转换
Voidprint(long);
Void print(double );
Void func(inta){
Print(a); //error 因为有二义性; 应该指明是print(long(a)),还是print(double(a));
}
b) 重载的使用说明
1. C++的函数如果在返回类型,参数类型,参数个数,参数顺序上有所不同,不构成重载
2. 让重载实现不事的功能是不好的编程风格
c) 重载的内部实现
C++采用名字粉碎法(name mangling)来改变函数名,以区分不同的同名函数
Int f(char a);
Int f(char a,int b, double c);
用上面的方法表示后f_c, f_cid
三.默认参数的函数
a) 默认参数的目的:容错性更强,利于反复调用
b) 默认参数的声明:当又有声明又有定义时,定义中不允许出现默认参数,应该在声明出给了默认值。如果函数只有定义,则默认参数才可以出现在函数定义中
c) 默认参数的传参顺序
默认参数应该从右到左逐渐定义
Void func(int a= 1, int b, int c=3, int d=4); //error
Void func(int a,int b = 2, int c=3, int d =r); //OK
Func(12, 12)//OK 参数C和D有默认
Func(2, 15, ,20) //error参数只参从右到左顺序匹配默认
d) 默认参数可以把一系列简单的重载函数合成为一个
Void point(int,int) {…}
Void point(int){ …}
Void point() {…}
可用下面的来代替:void point(int = 3, int = 4);
e) 默认值不可以是全局变量,因为默认值是在编译时确定的,而局部变量在编译时无法确定值
四.类
a) C和C++结构体的区别
i. 假如Savings为结构体:在C中定义的方法是structSavings a; 在C++中是Savings a;
ii. C++的类既能包含数据成员和成员函数
iii. C++类的限制关键定是:public, protected, private
iv. C++的结构体中的变量默认是Public,便可以加private做限制。类中默认的是private的
b) ::属于域区分符。可以指明方法或变量属于哪个类。可以不跟类名,表示全局数据
c) 在类中定义的成员函数一般都比较小,语句只有1~5句。特别的是不能使用switch语句。一般默认为内联函数,即使没有使用inline
d) 在类外进行函数定义
//Table.h
Class Table {
Public
Void set(…);
}
//table.cpp
Void Table::set(…){…}
e) 在本类的方法中调用本类的方法和变量无需加对象名或者类名,因为在使用变量或方法的时候相当于有一个this指针,指向被调用的变量或方法。
f) 可见性
Class Sample {}
Void func(intsample){ class Sample a; Sample++ }
Int s = 0;
Void func(){
Class s{};
S a; //定义一个类对象
::S = 3; //引用全局变量;
}
五.构造析构函数
a) 构造函数
i. 每个类必须有一个构造函数,如果没有,则使用默认构造函数。提供了构造函数后就不再提供无参的默认构造函数,除非已经定义了无参构造函数,否则无法使用无参构造函数
ii. 除了全局变量或静态变量外,对象的其他变量为随机值
iii. 在类内的变量只是一个声明,并非定义一个变量。假如,一个D类没有默认构造函数,在E类中定义变量D d;是可以的。而在函数中定义D d; 会调用D的默认构造函数,则产生错误。也不允许在类中D d = 9818;
iv. 在外部定义是需要加类名::类名();
v. 不可以有返回值,也不可以有void
vi. 可以Tdate dday(1,2); Tdate dday; 但不可以 Tdate dday(); 这样是定义了一个名字为dday,返回值为Tdate的函数。Tdateoneday(int); 为声明对象。Tdateoneday(10);为创建对象
vii. 由于构造函数用于创建对象,所以调用它来给对象赋值是错误的
viii. 使用默认参数可以合并多个类似的函数
ix. 重载构造函数若与参数默认值的构造函数发生冲突,则创建对象的语句会导致编译错误
Tdate(int d) {…}; 与 Tdate(int m, int 12) 造成冲突
x. 在单继承的情况下,会先创建父类,如果B继承A,C继承B,如果实例化C,则构造顺序是ABC。
xi. 同一个类继承多个同级的类时,按继承顺序从左到右创建父类,再创建本类。
xii. 如果B继承A,C继承B,在C类中包含对象A,D。则创建顺序是,先AB,再按变量列表的顺序构造变量A,D,即使有默认参数列表也会按照定义的顺序分配。最后构造本类C。因为要先知道AD的内存大小才好分配自身内存大小
xiii. 对类中的对象或变量进行赋值和初始化可以采用默认参数列表,Student(…, int sid):id(sid),其中,id为类中的D id;对象。而在构造函数里面就不是初始化,无法对引用或常量进行赋值,不可以Student(…, int sid):id=sid
xiv. 对一个变量赋值有以下方式
Main(){
int m=10;
int n(20); //TC+3.0不允许,C++新标准允许
n(20); //不是赋值,是调用n函数
Student s = “Jenny”;
Student t(“Danny”);
}
xv. 程序中的局部变量并不是由运行的顺序决定的,是在程序运行前已经统一定义了
xvi. 静态对象只被构造一次,文件作用域的静态变量在主函数开始运行时已经被构造完毕,块作用域的在首次运行时被构造
xvii. 所有的全局对象都在main函数之前被构造,但是如果该对象有一个错误,则无法得到控制权,导致程序BUG。解决这种方法有:一是将全局变量作为局部变量调试,二是打印出来观察
xviii. 全局对象构造时无特殊顺序,所以不能是一个全局变量赋值给另一个全局变量
b) 析构函数
i. ~类名();
ii. 没有返回类型,也没参数,也没有重载
六.动作内存分配
a) C++程序的内存格局通常分为四个区:
i. 数据区(data area):静态变量为一个区,全局变量为一个区,只读数据区,未定义数据区
ii. 代码区(code area):
iii. 栈区(stack area):局部变量,函数参数,返回数据,返回地址等存放在栈区
iv. 堆区(heap area)
b) 不用用malloc的原因是,malloc只分配空间,而类的建立是集空间分配,构造结构,以及初始化为一体
pD = (Tdate*)malloc(sizeof Tdate); p从malloc()中获得的是一个非法数据的类对象,对应的对象空间不确定。可以再调用内部接口p->setTdate()来初始化。但不实际
c) New不用显示指明指针类型,且自动调用构造函数。
pD=new Tdate(1,1, 1998); delete(pd);
pS= newTdate[10]; delete[] pS; //new数组时只能传个数,不能传参,只能调用默认构造函数。且释放的时候要写[],否则会产生错误,如果[]里面写了大小,则编译器会把他忽略
d) New一个整形数组的方法 int *v = new int[10];
七.拷贝构造函数
a) 拷贝函数的样式:类名(类名 & s)
b) 多处用到拷贝构造函数,如:Student s2=s1;. Void fn(Student fs){ return fs}; Student s = fn(u);
c) 为什么要用到拷贝构造:因为对象的复制不仅是二进制内存的复制,还有些对象资源或者有一些对象包含系统资源。用拷贝函数,新旧两个对象就一起共用了这个资源,有可能造成资源管理混乱
d) 浅复制:只作了变量的拷贝,资源不拷贝
e) 深复制:资源的拷贝,并把新的变量指向新的资源
八.无名对象
Void fn(Student&s);
Int main()
{
Student a=Student(“Randy”); //并不是无名对象,而是调用初始化构造一个变量。与Student a(“Randy”);类似
Student &refs = Student (“Randy”); //可以初始化引用
Student s = Student(“Jenny”); //初始化对象
Fn(Student(“Dany”));
Return 0;
}
第一个中,无名对象产生在main栈空间,他完全等价于Student refs=”Randy”; ???
第二个,用无名对象拷贝构造一个对象S,先调用构造函数再调用拷贝函数等价于 Student refs=”Randy”;
第三种,先创建一个无名变量,再复制到形参
// int &i=1;//不能用非对象去初始化一个引用。 ???
const int &j=1;//这样可以,但是没什么实际意义
string str("haha");
test(str);
test_const(str);
// test(string("haha"));//invalid initialization of non-const reference of type 'std::string&' from a temporary of type 'std::string'。使用临时对象不能初始化test的string & 引用。
test_const(string("haha"));//使用临时对象初始化函数形参的时候,函数形参必须是有const限定。
九.构造函数用于类型转换
a) 基本操作中,5/8与5.0/8不同,5.0/8匹配了double类型的除法。
b) C++会进行试探性转换,尝试构造一个类。如:
void fn(Student &s);
void main() {
fn(“Jenny”); //从String到Student类型的转换,C++会自动寻找Student(char *)构造函数
}
c) 但是要注意两点
i. 只会尝试含有一个参数的构造函数;
ii. 如果有二义性,则放弃尝试,如果void addCourse(Student &s)与void addCourse(Student &s);有二义性。只要显示转换一下即可addCourse(Teacher(“Prof.Dingleberry”))
十.静态成员的使用
a) 在对象空间中,没有静态成员的存在,不会随着对象的分配或析构。所有相同的类都共享一个静态成员
b) 在类中,定义静态成员:static int noOfStudent; //不可写成noOfStudent
c) 在类中,定义静态函数:static int number() { return noOfStudent }
d) 静态数据必须在一开始时就被初始化,不能在函数运行期间初始化。
e) 不能在头文件的类外部定义,这样会重复包含定义。
f) 不能在别的CPP中初始化,这样在另一个CPP中要再次初始化。
g) 可以在类的CPP下面初始化。也可以重用,如在Student.h中定义,在Student.cpp中初始化。Int Student::noOfStudent = 0;
h) 静态数据不属于哪个对象。公共静态数据成员可以被类的外部初始化,保护或私有静态数据成员只可被类的内部访问。公共静态成员的访问方法Student::noOfStudents, 不能用Student.noOfStudent
i) 下面的代码用返回对象引用的成员函数作为对象值去操作静态成员,但是静态成员只取返回对象的类型,其成员函数未被执行。引用静态成员时,C++只关心静态成员的类的类型
Class Student {
public:
staticint noOfStudents;
Student &nextStudent() {
noOfStudent++;
return this;
}
}
Void fn(Student&s) {
Cout<<s.nextStudent().noOfStudent<<endl;
}
Ina main() {
Student ss;
Fn(ss);
}
十一. 静态成员函数
a) 与静态成员变量类似。如果用对象去引用静态成员函数,只是用其类型。
b) 一个静态成员函数不与任何对象相联系,故不能对非静态成员变量进行访问。
Static char*sName() { //静态成员函数只认类型,与静态变量类似
Cout << noOfStudent <<endl;
Return name //访问非静态成员,不知道该成员属于哪个对象
}
c) 静态函数可以通过静态变量去访问内部数据,如pNext为静态变量,可以通过pNext->next去访问
d) 静态成员函数与非静态的区别在于,非静态有this指针指引,静态与对象无关,s1.findName(“jenny”),与Student::fineName(“jenny”);结果一样
Class Sc {
Public:
Void nsfn(int a); //像声明Sc::nsfn(Sc *this, int a);
Static void sfn(int a); //无this指针
}
Void f(Sc&s){
s.nsfn(10); //转换为Sc::nsfn(&s, 10)
s.sfn(10); //转换为Sc::sfn(10)
}
十二. 友元
a) 需要友元的原因:普通函数需要直接访问类的保护或私有的保护或私有数据成员的原因主要是为提高效率
b) 需要友元的另一个原因是方便重载操作符的使用
c) 在类的内部声明:
Class Student;//前向声明类名声明
Class Vector {
Public:
Void assignGrade(Student &s);
Friend Vector Multiplay(Matrix&m, Vector &v); //可在public区,也可以在protected区,完全一样
}
d) 一个类的成员函数可以是另一个类的友元
Class Student {
Public:
Friend void Vector::assignGrade(Student&s);;
}
VoidVector::assignGrades(Student &s) {
s.gpa = 4.0, //可以修改Student里面变量
}
e) 整个类可以是另一个类的友元,该友元称为友类。如,老师既可以修改成绩,又可以调整学时数
Class Student {
Public:
Friend class Teacher; //友类
}
Class Teacher {
Public :
Void assignGrades(Student &s); //修改成绩
Void adjustHours(Student&s); //修改学时数
}
十三. 继承
a) Class GraduateStudent: publicStudent
b) 派生类对象无法直接访问基类的资源。可以在派生类的方法中使用基类的资源
c) 继承的转换,
void fn(Student &s) {}
void main() {
GraduateStudent gs;
fn(gs); //相当于做了Student(gs)的类型转换。注意类型转换的方式,是否可见域也会发生改变??
}
d) 一般不会直接访问基类的数据,而是通过接口进行相互的数据交流
e) 同样是因为要知道基类占用内存大小,派生类会首先初始化基类,如果基类无法构造则造成错误。对基类数据成员的构造及初始化:
classGraduateStudent: public Student {
public:
GraduateStudent(char *pName,Advisor &adv)
:Student(pName),advisor(adv) //初始化adv会通过拷贝函数把值复制给advisor
}
f) 析构的顺序与构造顺序相反。先调用GraduateStudent,再析构advisor,最后析构Students
十四. 抽象类(abstract class)
a) 这样的类的唯一用途就是被继承,一个抽象类至少有一个纯虚函数。
b) 纯虚函数(pure virtual function)指不具体实现的虚成员函数
Virtual voidWidthdrawal(float tamount) = 0; // =0表明不定义该函数。为派生类保留位置。在派生类中也同样要声明虚函数,不加=0。定义加了0为纯虚函数。不加0为虚函数,可以在子类中不重写,直接调用
c) 一个抽象类不能有实例对象,即使不能由该类抽象来制造一个对象
Class Display {
Public:
Virtual void init() = 0; //纯虚函数
Virtual void write(char *pString)= 0; //纯虚函数
}
ClassMonochrome: public Display {
Virtual void init() ; //继承了init函数进行重写
Virtual void write(char *pString); //继承了write函数进行重写
}
ClassColorAdapter: public Display { //彩色显示器,
Virtual void write(char *pString) ; //继承write函数进行重写,但无法确定各种彩色显示器如何INIT的
}
ClassSVGA:public ColorAdapter { //VGA系列彩色显示器
Virtual void init(); //继承了ColorAdpter显示器写的方法,但是针对某种特定的SVGA,
//提供了不同的init方法
}
VoidMonochrome::init() {//省略
}
VoidMonochrome::write(char *pString) {
}
VoidColorAdapter::write(char *pString) {
}
VoidSVGA::init() {
}
十五. 多重继承
a) 多重继承class SleeperSofa :public Bed, public Sofa
b) C++继承的模糊性,假如Bed和Sofa都有一个weight,那该继承谁的呢。使用方法:ss.Sofa::SetWeight(20);因些程序员除了知道接口外,还要知道他的内部结构,不方便使用。
十六. 继承的访问权限
十七. 虚拟继承
a) 床和沙发都属于家具,且都有重量和设置重量显示重量的方法,沙发和床都继承了家具,同时沙发床又继承了沙发和床。所以对家具就有再次继承,编译不通过。C++提供了虚拟继承,保证相同的部份只有一个
Class Bed:virtual public Furniture { }
class Sofa:virtual public Furniture { }
class SleeperSofa:public Bed, public Sofa { }
b) 加上virtual 说明,如果没有Furniture类,则加入一个Furniture拷贝,否则就用一个
派生类的三种继承方式
公有继承public)、私有继承(private)和保护继承(protected)是常用的三种继承方式。
1.对于公有继承方式:
基类成员对其对象的可见性与一般类及其对象的可见性相同,公有成员可见,其他成员不可见。这里保护成员与私有成员相同。
基类成员对派生类的可见性对派生类来说,基类的公有成员和保护成员可见:基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态;基类的私有成员不可见:基类的私有成员仍然是私有的,派生类不可访问基类中的私有成员。
基类成员对派生类对象的可见性对派生类对象来说,基类的公有成员是可见的,其他成员是不可见。
所以,在公有继承时,派生类的对象可以访问基类中的公有成员;派生类的成员函数可以访问基类中的公有成员和保护成员。
2.对于私有继承方式:
基类成员对其对象的可见性与一般类及其对象的可见性相同,公有成员可见,其他成员不可见。
基类成员对派生类的可见性对派生类来说,基类的公有成员和保护成员是可见的:基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问;基类的私有成员是不可见的:派生类不可访问基类中的私有成员。
基类成员对派生类对象的可见性对派生类对象来说,基类的所有成员都是不可见的。
所以,在私有继承时,基类的成员只能由直接派生类访问,而无法再往下继承。
3.对于保护继承方式:
这种继承方式与私有继承方式的情况相同。两者的区别仅在于对派生类的成员而言,
基类成员对其对象的可见性与一般类及其对象的可见性相同,公有成员可见,其他成员不可见。
基类成员对派生类的可见性对派生类来说,基类的公有成员和保护成员是可见的:基类的公有成员和保护成员都作为派生类的保护成员,并且不能被这个派生类的子类所访问;基类的私有成员是不可见的:派生类不可访问基类中的私有成员。
基类成员对派生类对象的可见性对派生类对象来说,基类的所有成员都是不可见的。
所以,在保护继承时,基类的成员也只能由直接派生类访问,而无法再往下继承。
公有继承(public)
①继承后基类的公有成员、私有成员、保护成员在派生类中访问权限保持不变。
②在派生类中可以直接访问基类的公有成员和保护成员,但对于私有成员的访问只能通过基类的非私有成员函数间接访问。
③在基类和派生类定义以外只能通过派生类的对象访问基类的公有成员,无法通过派生类对象直接访问基类的私有成员和保护成员。
私有继承(private)
①继承后基类的所有成员在派生类中均为私有成员。
②在派生类中可以直接访问基类的公有成员和保护成员,但对于私有成员的访问只能通过基类的非私有成员函数间接访问。
③在基类和派生类定义以外对基类的所有成员均无法直接访问也无法通过派生类的对象间接访问。
保护继承(protected)
①继承后基类的公有成员和保护成员在派生类中均为保护成员,基类的私有成员在派生类中仍为私有成员。
②在派生类中可以直接访问基类的公有成员和保护成员,但对于私有成员的访问只能通过基类的非私有成员函数间接访问。
③在基类和派生类定义以外对基类的所有成员均无法直接访问也无法通过派生类的对象间接访问。
十八. 多重继承的构造顺序
a) 构造对象的规则需要扩展以控制多重继承。构造函数按下列顺序被调用:
(1)任何 虚拟基类的构造函数按照它们被继承的顺序构造;(#add 即声明顺序)
(2)任何非虚拟基类的构造函数按照它们被继承的顺序构造;
(3)任何成员对象的构造函数按照它们声明的顺序调用;
(4)类自己的构造函数。
十九. 赋值兼容规则和多态
a) 赋值兼容规则中所指的替代包括以下情况
i. 公有派生类的对象可以赋值给基类的对象,即将公有派生类对象中从基类继承而来的数据成员逐个赋值给基类对象的对应数据成员;
ii. 公有派生类对象的地址可以赋值给基类的对象指针;
iii. 公有派生类对象可以用来初始化基类的对象引用。
b) 在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承成员
c) 赋值兼容有局限性,而多太的设计方法可以保证在赋值兼容的前提下,基类、派生类分别以不同的方式来响应相同的消息。
d) 运行过程中的多态需要满足三个条件:首先类之间应满足赋值兼容规则,其二是要声明虚函数,第三是要由成员函数来调用或者是通过指针、引用来访问虚函数。
e) 如果派生类没有显示给出虚函数声明,这时系统就会遵循以下规则为判断一个函数是否为虚函数
i. 该函数是否与基类的虚函数有相同的名称
ii. 该函数是否与基类的虚函数有相同的参数个数及相同的对应参数类型
iii. 该函数是否与基类的虚函数有相同的返回值或者满足赋值兼容规则的指针,引用型的返回值
iv.