C++ 点滴积累(4)
2011-10-22 23:45
162 查看
1. 数组作形参
声明:在C/C++中,数组参数永远不会按值传递,它是传递第一个元素(准确地说是第0个)的地址,就是说形参不会复制数组。
- 例如:void putValues(int[10]);
- 被编译器视为:void putValues(int *);
- 数组的长度与参数声明无关,因此,下列三个声明是等价的:
void putValues(int *);
void putValues(int[]);
void putValues(int [10] );
因此数组被传递为指针,所以这对程序员有两层含义。
如何修改?
一个常见的机制是提供一个含有数组长度的额外参数。
例如:void putValues(int [], int size);
另外一种机制是将函数参数声明为数组的引用。
当参数是一个数组类型的引用时,数组长度成为参数类型的一部分,编译器将检查数组实参的长度是否匹配。
运行结果:(10)<1,0,0,0,0,0,0,0,0,0>
2.对象数组初始化
创建数组中每一个元素对象时,系统都会调用类构造函数初始化该对象。
通过初始化列表赋初值。
– 例: Point A[2]={Point(1,2),Point(3,4)};//注意理解
如果没有为数组元素指定显式初始值,数组元素便调用无参构造函数。
各元素对象的初值要求为相同值时,要声明具有默认形参值的构造函数。
各元素对象的初值要求为不同值时,要声明带形参的构造函数。
各元素对象的初值要求为不同值时,要声明带形参的构造函数。
当数组中每一个对象被删除时,系统都要调用一次析构函数。
3.为什么不能用一个内部auto变量去初始化static指针?
因为内部auto变量所对应的存储单元随函数的调用而存在,随函数的执行完毕而回收,而静态指针却长期占用内存,不随函数的调用或执行结束而释放,
当再次进入函数后该指针又成为可见的.因此,用内部auto变量的地址去初始化一个静态指针是没有意义的。
4. 类的聚集和浅拷贝与深拷贝
详见:/article/7786083.html
5.用字符数组存储和处理字符串
1)字符串的输入/输出
方法
–逐个字符输入输出
–将整个字符串一次输入或输出
例:char c[]="China";cout<<c;
注意
–输出字符不包括'\0'
–输出字符串时,输出项是字符数组名,输出时遇到'\0'结束。
–输入多个字符串时,以空格分隔;输入单个字符串时其中不能有空格。
例如:程序中有下列语句:
static char str1[5],str2[5],str3[5];
cin>>str1>>str2>>str3;
运行时输入数据:How are you?
内存中变量状态如下:str1: H o w \0 str2: a r e \0 str3: y o u ? \0
若改为:
static char str[13];
cin>>str;
运行时输入数据:How are you?
内存中变量str内容如下:str: H o w \0
注意!若有如下声明:
char a[4], *p1, *p2;
–错误的:
a="abc"; (数组名是常量,只能当左值用)
cin>>p1; (local variable 'p1' used without having been initialized)
–正确的:
p1="abc";
p2=a; cin>>p2;
2)整行输入字符串
cin.getline(字符数组名St, 字符个数N, 结束符);
功能:一次连续读入多个字符(可以包括空格),直到读满N个,或遇到指定的结束符(默认为'\n')。
读入的字符串存放于字符数组St中。读取但不存储结束符。
cin.get(字符数组名St, 字符个数N, 结束符);
功能:一次连续读入多个字符(可以包括空格),直到读满N个,或遇到指定的结束符(默认为'\n')。
读入的字符串存放于字符数组St中。既不读取也不存储结束符。
dalian, china
City: dalian Country: china
beijing, china
City: beijing Country: china
Press any key to continue
3)字符串处理函数
strcat(连接),strcpy(复制),
strcmp(比较),strlen(求长度),
strlwr(转换为小写),
strupr(转换为大写)
头文件<cstring>
strlen()函数与sizeof的差异
sizeof()是测试一个变量所占字节数;
strlen()是测试一个串有效字符个数。
char a[10] ="123456789" ,*p = a;
printf("%d\n",sizeof(a)); // 10
printf("%d\n",strlen(a)); // 9
printf("%d\n",sizeof(p)); // 4
printf("%d\n",strlen(p)); // 9
尤其是当指针作形参,字符型数组作实参时,定要搞清形参究竟得到了什么。
4)string类
面向对象的“串”,已不再是面向过程的“串”了。它是类。除了含有容纳串的字符序列外,还含有丰富的成员函数,以便提供串操作的服务。
该类在头文件string中。
6.类的继承与派生
1)继承与派生的目的
继承的目的:实现代码重用。
派生的目的:当新的问题出现,原有程序无法解决(或不能完全解决)时,需要对原有程序进行改造。
继承为软件的层次化开发提供了保证。
2)三种继承方式
–公有继承public (原封不动)
–保护继承protected (折中)
–私有继承private (化公为私)
继承方式影响子类的访问权限:
–派生类成员对基类成员的访问权限
–通过派生类对象对基类成员的访问权限
3)继承的工作内容
三项工作:
–吸收基类成员全盘(除了六个特别函数及静态外)接收;此项工作程序员无法干预。(不再讨论)
–改造基类成员
1.对基类成员访问权限的改变;(规则严格)
2.对基类成员的覆盖;(有技巧)
–新增派生类特有的成员“青出于蓝而胜于蓝”(自由发挥)
4)公有继承(public)
基类的public和protected成员的访问属性在派生类中保持不变,但基类的private成员不可直接访问。
派生类中的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员。
通过派生类的对象只能访问基类的public成员。
(前两条属类内访问,后条属类外访问)
保护继承(protected)
基类的public和protected成员都以protected身份出现在派生类中,但基类的private成员不可直接访问。
派生类中的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员。
通过派生类的对象不能直接访问基类中的任何成员
(前两条属类内访问,后条属类外访问)
protected 成员的特点与作用
对于本类模块来说,它与private 成员的性质相同,只供类内访问。
对于其派生类来说,它没变得不可访问,仍然保持了原来的性质。
既实现了数据隐藏,又方便了子类实现代码重用
私有继承(private)
基类的public和protected成员都以private身份出现在派生类中,但基类的private成员不可直接访问。
派生类中的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员。
通过派生类的对象不能直接访问基类中的任何成员。
(前两条属类内访问,后条属类外访问)
由于私有继承,原有的公有成员和保护成员的访问级别升高成了私有级。若想保持原访问权限不变,可以用::将其还原,即所谓“捞”出来。但是,
第一:不能区分重载。
第二:只能还原,不能提高访问级别。
保护继承的还原亦同理。
5)继承与静态成员
类的静态成员不参与继承,当然也不受继承方式的影响。
子孙们类都可访问基类的静态成员。
类外对静态成员的访问,取决于该成员的访问权限。
6)三种继承方式的选择
若想完全保留基类的操作功能,只是扩展新功能,则用公有继承。这叫“类型继承”。
若想完全改变基类的功能,将其改头换面,做成基于原来类,但隐藏、伪装了原功能,则用私有继承。只能算“实现继承”。
若想既隐藏基类的操作功能,又能方便的传给后代,不至于难以访问,则用保护继承。也是“实现继承”。
7)类型兼容规则
一个公有派生类的对象可以替代基类的对象(反之则禁止):
–派生类的对象可以被赋值给基类对象。
–派生类的对象可以初始化基类的引用。
–指向基类的指针也可以指向派生类对象。
通过基类对象名、引用名、指针只能使用从基类继承来的成员。“窄化效应”,“切割”。
此规则又称“类型包容法则”、“向上兼容性”、“向上映射”。
“类型兼容规则”无论对于单继承还是多继承皆适用。
8)继承时的构造函数
单继承时构造函数的形式
派生类名::派生类名(基类所需的形参,本类成员所需的形参): 基类名(参数表) //初始化列表
{本类成员赋初值语句;}
多继承时的构造函数
派生类名::派生类名
( 基类1形参,基类2形参,... 基类n形参,
本类形参)
: 基类名1(参数), 基类名2(参数), ...基类名n(参数) //初始化列表
{
本类成员赋初值语句;
}
多继承且有内嵌对象时的构造函数
派生类名::派生类名
(基类1形参,基类2形参,...基类n形参,
本类形参)
: 基类1(参数), 基类2(参数), ...基类n(参数),
对象数据成员的初始化
{
本类成员赋初值语句;
}
派生类与基类构造函数的关系
当基类中未声明任何构造函数或声明了无参构造函数时,派生类构造函数可以不向基类构造函数传递参数。
若基类中未声明构造函数,派生类中也可以不声明,全采用缺省形式构造函数。
当基类声明有带参构造函数时,派生类也必须声明带参构造函数,并将参数传递给基类构造函数。
constructing B2 2
constructing B1 1
constructing B3 *
constructing B1 3
constructing B2 4
constructing B3 *
Press any key to continue
构造函数的调用次序
1.首先调用基类构造函数,调用顺序按照它们被继承时声明的顺序(从左向右)。
2.然后调用成员对象的构造函数,调用顺序按照它们在类中声明的顺序。
3.最后,执行派生类的构造函数体中的语句。
9)继承时的拷贝构造函数
若建立派生类对象时调用缺省拷贝构造函数,则编译器将自动调用基类的缺省拷贝构造函数。
若基类有显式拷贝构造函数且需要传递参数,则定要为派生类编写拷贝构造函数,以便为基类相应的拷贝构造函数传递参数。
B0::display() m = 10
D1::display() 20
B0::display() m = 10
D1::display() 20
Press any key to continue
小结:使用初始化列表的场合
当类关系是组合时,为作为成员的对象隐式调用构造函数,产生有名对象之用。此时初始化列表为构造函数传递实参。
当类关系是继承时,为作为子类组成部分的父类成员显式调用构造函数,产生无名对象之用。此时初始化列表也为构造函数传递实参。
也可以为类自身的数据成员赋初值之用。尤其是为常数据成员和引用型数据成员初始化时之用。
10)继承时的析构函数
析构函数也不被继承,派生类自行声明
声明方法与一般(无继承关系时)类的析构函数相同。
不需要显式地调用基类的析构函数,系统会自动隐式调用。
析构函数的调用次序与构造函数相反。
7. ::用法小结
详见:/article/7786085.html
8. 二义性问题
详见:/article/7786084.html
9. 多态性
1)多态性的分类
2)多态性的实现
多态的实现可分为编译时多态和运行时多态,它们分别对应静态联编和动态联编。
联编又称为绑定(binding),是指计算机程序中的语法元素(标识符、函数等)彼此相关联的过程。
从绑定的时机看,在编译时就完成的绑定叫静态绑定;直到运行时才能确定并完成的绑定叫动态绑定。
静态绑定消耗编译时间,动态绑定消耗运行时间。
静态绑定的程序到了运行阶段其功能就固定了,即使情况发生了变化,功能无法改变。
动态绑定的程序由于绑定发生在运行阶段,其功能是未定的,当情况变化了,功能也跟着变。于是表现出会聪明的判断及具有灵活的行为。
关于“抽象”
指针——地址的抽象;
形参——数值的抽象;
对象——事物的抽象;
类——对象的抽象;
超类——类的抽象;
模板——类型的抽象;
多态——行为的抽象;
函数——过程的抽象;
类型——数据标识的抽象;
异常——错误的抽象;
函数对象——函数的一元化抽象;
流——文件的抽象;
3)运算符重载
运算符重载是对已有的运算符赋予多重含义。其实是将运算符函数化。
基本认识–C++中预定义的运算符其运算对象只能是基本数据类型,而不适用于用户自定义类型(如类)。
实现机制–将指定的运算表达式转化为对运算符函数的调用,运算对象转化为运算符函数的实参。
–编译系统对重载运算符的选择,遵循函数重载的选择原则。
规则和限制
可以重载C++中除下列运算符外的所有运算符: . .* :: ?: sizeof || &&
只能重载C++语言中已有的运算符,不可臆造新的。
不改变原运算符的优先级和结合性。
不能改变操作数个数。
经重载的运算符,其操作数中至少应该有一个是自定义类型,即不可重写原运算符。
不可声明为类属性。
重载函数的表现形式仅有两种:
重载为类成员函数。–要受类成员的存取权限约束
重载为友元函数。
声明形式:函数类型 operator 运算符(形参表){......}
重载为类成员函数时:形参个数=原操作数个数-1(后置++、--除外)
重载为友元函数时:形参个数=原操作数个数
运算符成员函数的设计
c1=(5,4)
c2=(2,10)
c3=c1-c2=(3,-6)
c3=c1+c2=(7,14)
Press any key to continue
First time output:23:59:59
Show myClock++:23:59:59
Show ++myClock:0:0:1
Press any key to continue
运算符友元函数的设计
运算符 定义式 等价式
双目运算符@ oprd1 @oprd2 operator @(oprd1,oprd2)
前置单目@ @ oprd operator @(oprd)
后置单目@ oprd@operator @(oprd,0 )
如果需要重载一个运算符,使之能够用于在类外操作某类对象的私有成员,可以此将运算符重载为该类的友元函数。
函数的形参代表了依自左至右次序排列的各操作数。
为了区分前置后置运算,后置单目运算符++和--的重载函数,其形参表中要增加一个int,但不必写形参名。
运算符是: 建议设计为:
全部单目运算符 非静态成员函数
= () [ ] -> * 必须是非静态成员函数
+= -= *= /= %= 非静态成员函数
<<= >>= ^= |= &= 非静态成员函数
双目运算符 友元函数
>> << 必须是友元函数
虚函数 成员函数
若允许链式运算 该函数应返回本类型的引用
若定义了+ -* / % 千万别忘了定义=
对于任何形参,若仅读取值而不修改,则应设为const &;
函数的返回值的类型,可以是参数对象的类型(如+),也可以异于参数对象(如<=);
所有的赋值运算符皆可以改变左值。为满足链式运算要求,函数应返回同类型的非常引用。(记住:编译器是自左向右扫描表达式的,但却是从右向左来处理的。)
对于逻辑运算符,应返回bool类型,至少是int型;
对于自增自减运算符,可以返回本类的对象,亦可处理成返回是否继续迭代的逻辑值;
下标运算符必须是成员函数,单个形参,返回是引用;
()运算符是唯一允许带任意个参数的函数,必须设计为成员函数;
指针运算符->最神奇也最复杂,不但重载了,甚至演化为类,作用于容器,叫作迭代子。
4)虚函数
虚函数是动态绑定的技术基础。
是非静态的成员函数。
在类的声明中,在函数原型之前写virtual。
virtual只用来说明类声明中的原型,不能用在函数实现时。
具有继承性,基类中声明了虚函数,派生类中无论是否说明,同原型函数都自动为虚函数。
本质:不是重载(overload)而是覆盖(override)。
调用方式:通过基类指针或引用,执行时会根据指针指向的对象的类型,决定调用哪个类的函数。
虚函数的实现机制
编译器发现某类含有虚函数,则对其生成的对象悄悄地加入一个void 型的指向指针的指针vptr,并让其指向一个虚函数表vtable(其实是个指针数组),每个表项是一个虚函数名,排列次序按虚函数声明的次序排列。
在类族中,无论是基类还是派生类,都拥有各自的vptr和vtable。相同类型所生成的对象拥有相同的vtable。
派生类新增的虚函数依次排在后面。当然,派生类的vtable表项中放的是新的覆盖函数的首址。
动态多态的前提
(缺一不可)
必须有继承产生的类族;
必须是公有继承(类型兼容);
派生类的成员函数要重写该虚函数;
基类的某成员函数使用了virtual;
派生类的对象要使用指针或引用来调用该虚函数;
虚析构函数
为何需要虚析构函数?
避免析构对象不彻底。
何时需要虚析构函数?
当一个类含有虚函数时。
因为,当你打算用基类指针(或引用)删除子类对象,由于切割现象,会只释放基类部分,这会遗留内存垃圾。让基类的析构函数成为虚函数,则会彻底释放内存。
10. 局部类,内嵌类,抽象类
详见:/article/7786086.html
声明:在C/C++中,数组参数永远不会按值传递,它是传递第一个元素(准确地说是第0个)的地址,就是说形参不会复制数组。
- 例如:void putValues(int[10]);
- 被编译器视为:void putValues(int *);
- 数组的长度与参数声明无关,因此,下列三个声明是等价的:
void putValues(int *);
void putValues(int[]);
void putValues(int [10] );
因此数组被传递为指针,所以这对程序员有两层含义。
如何修改?
一个常见的机制是提供一个含有数组长度的额外参数。
例如:void putValues(int [], int size);
另外一种机制是将函数参数声明为数组的引用。
当参数是一个数组类型的引用时,数组长度成为参数类型的一部分,编译器将检查数组实参的长度是否匹配。
#include <iostream> #include <string> using namespace std; void putValues(int(&arr)[10]); void main() { int i, j[2],k[10] = {1}; //putValues(i);//错误:实参的类型不符 //putValues(j);//错误:实参不是10个int的数组 putValues(k); } void putValues(int(&ia)[10]) { cout<<"(10)<"; for(int i = 0;i < 10; ++i) { cout<<ia[i]; if(i!= 9) cout<<",";//用逗号分隔元素 } cout<<">\n"; }
运行结果:(10)<1,0,0,0,0,0,0,0,0,0>
2.对象数组初始化
创建数组中每一个元素对象时,系统都会调用类构造函数初始化该对象。
通过初始化列表赋初值。
– 例: Point A[2]={Point(1,2),Point(3,4)};//注意理解
如果没有为数组元素指定显式初始值,数组元素便调用无参构造函数。
各元素对象的初值要求为相同值时,要声明具有默认形参值的构造函数。
各元素对象的初值要求为不同值时,要声明带形参的构造函数。
各元素对象的初值要求为不同值时,要声明带形参的构造函数。
当数组中每一个对象被删除时,系统都要调用一次析构函数。
3.为什么不能用一个内部auto变量去初始化static指针?
因为内部auto变量所对应的存储单元随函数的调用而存在,随函数的执行完毕而回收,而静态指针却长期占用内存,不随函数的调用或执行结束而释放,
当再次进入函数后该指针又成为可见的.因此,用内部auto变量的地址去初始化一个静态指针是没有意义的。
4. 类的聚集和浅拷贝与深拷贝
详见:/article/7786083.html
5.用字符数组存储和处理字符串
1)字符串的输入/输出
方法
–逐个字符输入输出
–将整个字符串一次输入或输出
例:char c[]="China";cout<<c;
注意
–输出字符不包括'\0'
–输出字符串时,输出项是字符数组名,输出时遇到'\0'结束。
–输入多个字符串时,以空格分隔;输入单个字符串时其中不能有空格。
例如:程序中有下列语句:
static char str1[5],str2[5],str3[5];
cin>>str1>>str2>>str3;
运行时输入数据:How are you?
内存中变量状态如下:str1: H o w \0 str2: a r e \0 str3: y o u ? \0
若改为:
static char str[13];
cin>>str;
运行时输入数据:How are you?
内存中变量str内容如下:str: H o w \0
注意!若有如下声明:
char a[4], *p1, *p2;
–错误的:
a="abc"; (数组名是常量,只能当左值用)
cin>>p1; (local variable 'p1' used without having been initialized)
–正确的:
p1="abc";
p2=a; cin>>p2;
2)整行输入字符串
cin.getline(字符数组名St, 字符个数N, 结束符);
功能:一次连续读入多个字符(可以包括空格),直到读满N个,或遇到指定的结束符(默认为'\n')。
读入的字符串存放于字符数组St中。读取但不存储结束符。
cin.get(字符数组名St, 字符个数N, 结束符);
功能:一次连续读入多个字符(可以包括空格),直到读满N个,或遇到指定的结束符(默认为'\n')。
读入的字符串存放于字符数组St中。既不读取也不存储结束符。
//整行输入字符串举例 #include <iostream> using namespace std; void main (void) { char city[80]; char state[80]; int i; for (i = 0; i < 2; i++) { cin.getline(city,80,','); cin.getline(state,80,'\n'); cout<< "City: " << city << " Country: "<< state << endl; } }运行结果:
dalian, china
City: dalian Country: china
beijing, china
City: beijing Country: china
Press any key to continue
3)字符串处理函数
strcat(连接),strcpy(复制),
strcmp(比较),strlen(求长度),
strlwr(转换为小写),
strupr(转换为大写)
头文件<cstring>
strlen()函数与sizeof的差异
sizeof()是测试一个变量所占字节数;
strlen()是测试一个串有效字符个数。
char a[10] ="123456789" ,*p = a;
printf("%d\n",sizeof(a)); // 10
printf("%d\n",strlen(a)); // 9
printf("%d\n",sizeof(p)); // 4
printf("%d\n",strlen(p)); // 9
尤其是当指针作形参,字符型数组作实参时,定要搞清形参究竟得到了什么。
4)string类
面向对象的“串”,已不再是面向过程的“串”了。它是类。除了含有容纳串的字符序列外,还含有丰富的成员函数,以便提供串操作的服务。
该类在头文件string中。
#include <string> #include <iostream> using namespace std ; void trueFalse(int x) { cout<< (x? "True": "False") << endl; } void main() { string S1="DEF", S2="123"; char CP1[ ]="ABC"; char CP2[ ]="DEF"; cout<< "S1 is " << S1 << endl; cout<< "S2 is " << S2 << endl; cout<<"length of S2:"<<S2.length()<<endl; cout<< "CP1 is " << CP1 << endl; cout<< "CP2 is " << CP2 << endl; cout<< "S1<=CP1 returned "; trueFalse(S1<=CP1); cout<< "CP2<=S1 returned "; trueFalse(CP2<=S1); S2+=S1; cout<<"S2=S2+S1:"<<S2<<endl; cout<<"length of S2:"<<S2.length()<<endl; }
6.类的继承与派生
1)继承与派生的目的
继承的目的:实现代码重用。
派生的目的:当新的问题出现,原有程序无法解决(或不能完全解决)时,需要对原有程序进行改造。
继承为软件的层次化开发提供了保证。
2)三种继承方式
–公有继承public (原封不动)
–保护继承protected (折中)
–私有继承private (化公为私)
继承方式影响子类的访问权限:
–派生类成员对基类成员的访问权限
–通过派生类对象对基类成员的访问权限
3)继承的工作内容
三项工作:
–吸收基类成员全盘(除了六个特别函数及静态外)接收;此项工作程序员无法干预。(不再讨论)
–改造基类成员
1.对基类成员访问权限的改变;(规则严格)
2.对基类成员的覆盖;(有技巧)
–新增派生类特有的成员“青出于蓝而胜于蓝”(自由发挥)
4)公有继承(public)
基类的public和protected成员的访问属性在派生类中保持不变,但基类的private成员不可直接访问。
派生类中的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员。
通过派生类的对象只能访问基类的public成员。
(前两条属类内访问,后条属类外访问)
//公有继承举例 #include<iostream> #include<cmath> using namespace std; class Point//基类Point类的声明 { public://公有函数成员 void InitP(float xx=0, float yy=0) {X=xx;Y=yy;} void Move(float xOff, float yOff) {X+=xOff;Y+=yOff;} float GetX() {return X;} float GetY() {return Y;} private://私有数据成员 float X,Y; }; class Rectangle: public Point //派生类声明 { public://新增的公有函数成员 void InitR(float x, float y, float w, float h) {InitP(x,y);W=w;H=h;}//调用基类公有成员函数 float GetH() {return H;} float GetW() {return W;} private://新增的私有数据成员 float W,H; }; void main() { Rectangle rect; //用子类对象访问子类新增成员,间接访问了父类成员。 rect.InitR(2,3,20,10); rect.Move(3,2); //通过子类对象直接访问父类成员 cout<<rect.GetX()<<',' <<rect.GetY()<<',' <<rect.GetH()<<','<<rect.GetW()<<endl; }
保护继承(protected)
基类的public和protected成员都以protected身份出现在派生类中,但基类的private成员不可直接访问。
派生类中的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员。
通过派生类的对象不能直接访问基类中的任何成员
(前两条属类内访问,后条属类外访问)
protected 成员的特点与作用
对于本类模块来说,它与private 成员的性质相同,只供类内访问。
对于其派生类来说,它没变得不可访问,仍然保持了原来的性质。
既实现了数据隐藏,又方便了子类实现代码重用
//保护继承举例 #include<iostream> #include<cmath> using namespace std; class A { protected: int x; public: A(){x = 0;} A(int xx):x(xx){} void show() {cout << x << endl;} }; class B: protected A { public: void Function(); //只写函数名,不要带函数类型。数据成员亦同。 using A::show; }; void B::Function() { x = 5; } void main() { B b; b.Function(); b.show(); }
私有继承(private)
基类的public和protected成员都以private身份出现在派生类中,但基类的private成员不可直接访问。
派生类中的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员。
通过派生类的对象不能直接访问基类中的任何成员。
(前两条属类内访问,后条属类外访问)
//私有继承举例 #include<iostream> using namespace std; class Point//基类Point类的声明 { public://公有函数成员 void InitP(float xx=0, float yy=0) {X=xx;Y=yy;} void Move(float xOff, float yOff) {X+=xOff;Y+=yOff;} float GetX() {return X;} float GetY() {return Y;} private://私有数据成员 float X,Y; }; class Rectangle: private Point//派生类声明 { public://新增外部接口 void InitR(float x, float y, float w, float h) {InitP(x,y);W=w;H=h;}//访问基类公有成员 //子类要为基类提供被访问渠道。典型的“改造” void Move(float xOff, float yOff) {Point::Move(xOff,yOff);} float GetX() {return Point::GetX();} float GetY() {return Point::GetY();} float GetH() {return H;} float GetW() {return W;} private://新增私有数据 float W,H; }; int main() { Rectangle rect; rect.InitR(2,3,20,10); //通过派生类对象只能访问本类成员 rect.Move(3,2); cout<<rect.GetX()<<',' <<rect.GetY()<<',' <<rect.GetH()<<','<<rect.GetW()<<endl; return 0; }
由于私有继承,原有的公有成员和保护成员的访问级别升高成了私有级。若想保持原访问权限不变,可以用::将其还原,即所谓“捞”出来。但是,
第一:不能区分重载。
第二:只能还原,不能提高访问级别。
保护继承的还原亦同理。
5)继承与静态成员
类的静态成员不参与继承,当然也不受继承方式的影响。
子孙们类都可访问基类的静态成员。
类外对静态成员的访问,取决于该成员的访问权限。
6)三种继承方式的选择
若想完全保留基类的操作功能,只是扩展新功能,则用公有继承。这叫“类型继承”。
若想完全改变基类的功能,将其改头换面,做成基于原来类,但隐藏、伪装了原功能,则用私有继承。只能算“实现继承”。
若想既隐藏基类的操作功能,又能方便的传给后代,不至于难以访问,则用保护继承。也是“实现继承”。
7)类型兼容规则
一个公有派生类的对象可以替代基类的对象(反之则禁止):
–派生类的对象可以被赋值给基类对象。
–派生类的对象可以初始化基类的引用。
–指向基类的指针也可以指向派生类对象。
通过基类对象名、引用名、指针只能使用从基类继承来的成员。“窄化效应”,“切割”。
此规则又称“类型包容法则”、“向上兼容性”、“向上映射”。
“类型兼容规则”无论对于单继承还是多继承皆适用。
8)继承时的构造函数
单继承时构造函数的形式
派生类名::派生类名(基类所需的形参,本类成员所需的形参): 基类名(参数表) //初始化列表
{本类成员赋初值语句;}
多继承时的构造函数
派生类名::派生类名
( 基类1形参,基类2形参,... 基类n形参,
本类形参)
: 基类名1(参数), 基类名2(参数), ...基类名n(参数) //初始化列表
{
本类成员赋初值语句;
}
多继承且有内嵌对象时的构造函数
派生类名::派生类名
(基类1形参,基类2形参,...基类n形参,
本类形参)
: 基类1(参数), 基类2(参数), ...基类n(参数),
对象数据成员的初始化
{
本类成员赋初值语句;
}
派生类与基类构造函数的关系
当基类中未声明任何构造函数或声明了无参构造函数时,派生类构造函数可以不向基类构造函数传递参数。
若基类中未声明构造函数,派生类中也可以不声明,全采用缺省形式构造函数。
当基类声明有带参构造函数时,派生类也必须声明带参构造函数,并将参数传递给基类构造函数。
//派生类构造函数举例 #include <iostream> using namespace std; class B1//基类B1,构造函数有参 { public: B1(int i) {cout<<"constructing B1 "<<i<<endl;} }; class B2//基类B2,构造函数有参 { public: B2(int j) {cout<<"constructing B2 "<<j<<endl;} }; class B3//基类B3,构造函数无参 { public: B3() {cout<<"constructing B3 *"<<endl;} }; class C: public B2, public B1, public B3 { public://派生类的公有成员 C(int a, int b, int c, int d):B1(a),memberB2(d),memberB1(c),B2(b){} private://派生类的私有对象成员 B1 memberB1; B2 memberB2; B3 memberB3; }; void main() { C obj(1,2,3,4); }运行结果:
constructing B2 2
constructing B1 1
constructing B3 *
constructing B1 3
constructing B2 4
constructing B3 *
Press any key to continue
构造函数的调用次序
1.首先调用基类构造函数,调用顺序按照它们被继承时声明的顺序(从左向右)。
2.然后调用成员对象的构造函数,调用顺序按照它们在类中声明的顺序。
3.最后,执行派生类的构造函数体中的语句。
9)继承时的拷贝构造函数
若建立派生类对象时调用缺省拷贝构造函数,则编译器将自动调用基类的缺省拷贝构造函数。
若基类有显式拷贝构造函数且需要传递参数,则定要为派生类编写拷贝构造函数,以便为基类相应的拷贝构造函数传递参数。
//私有继承时的拷贝构造函数 #include <iostream> using namespace std; class B0//基类B0声明 { int m; public: B0() { m = 10; } B0(B0 & a) : m(a.m) { } void display() {cout<<"B0::display() m = "<<m<<endl;} }; class D: private B0 { int n; public: D(){ n = 20; } //用到类型兼容规则 D(D & b):B0(b),n(b.n){} void display() { B0:: display(); cout<<"D1::display()"<<" "<<n<<endl; } }; void main()//主函数 { D d1; D d2(d1); d1.display(); d2.display(); }运行结果:
B0::display() m = 10
D1::display() 20
B0::display() m = 10
D1::display() 20
Press any key to continue
小结:使用初始化列表的场合
当类关系是组合时,为作为成员的对象隐式调用构造函数,产生有名对象之用。此时初始化列表为构造函数传递实参。
当类关系是继承时,为作为子类组成部分的父类成员显式调用构造函数,产生无名对象之用。此时初始化列表也为构造函数传递实参。
也可以为类自身的数据成员赋初值之用。尤其是为常数据成员和引用型数据成员初始化时之用。
10)继承时的析构函数
析构函数也不被继承,派生类自行声明
声明方法与一般(无继承关系时)类的析构函数相同。
不需要显式地调用基类的析构函数,系统会自动隐式调用。
析构函数的调用次序与构造函数相反。
7. ::用法小结
详见:/article/7786085.html
8. 二义性问题
详见:/article/7786084.html
9. 多态性
1)多态性的分类
2)多态性的实现
多态的实现可分为编译时多态和运行时多态,它们分别对应静态联编和动态联编。
联编又称为绑定(binding),是指计算机程序中的语法元素(标识符、函数等)彼此相关联的过程。
从绑定的时机看,在编译时就完成的绑定叫静态绑定;直到运行时才能确定并完成的绑定叫动态绑定。
静态绑定消耗编译时间,动态绑定消耗运行时间。
静态绑定的程序到了运行阶段其功能就固定了,即使情况发生了变化,功能无法改变。
动态绑定的程序由于绑定发生在运行阶段,其功能是未定的,当情况变化了,功能也跟着变。于是表现出会聪明的判断及具有灵活的行为。
关于“抽象”
指针——地址的抽象;
形参——数值的抽象;
对象——事物的抽象;
类——对象的抽象;
超类——类的抽象;
模板——类型的抽象;
多态——行为的抽象;
函数——过程的抽象;
类型——数据标识的抽象;
异常——错误的抽象;
函数对象——函数的一元化抽象;
流——文件的抽象;
3)运算符重载
运算符重载是对已有的运算符赋予多重含义。其实是将运算符函数化。
基本认识–C++中预定义的运算符其运算对象只能是基本数据类型,而不适用于用户自定义类型(如类)。
实现机制–将指定的运算表达式转化为对运算符函数的调用,运算对象转化为运算符函数的实参。
–编译系统对重载运算符的选择,遵循函数重载的选择原则。
规则和限制
可以重载C++中除下列运算符外的所有运算符: . .* :: ?: sizeof || &&
只能重载C++语言中已有的运算符,不可臆造新的。
不改变原运算符的优先级和结合性。
不能改变操作数个数。
经重载的运算符,其操作数中至少应该有一个是自定义类型,即不可重写原运算符。
不可声明为类属性。
重载函数的表现形式仅有两种:
重载为类成员函数。–要受类成员的存取权限约束
重载为友元函数。
声明形式:函数类型 operator 运算符(形参表){......}
重载为类成员函数时:形参个数=原操作数个数-1(后置++、--除外)
重载为友元函数时:形参个数=原操作数个数
运算符成员函数的设计
//将“+”、“-”运算重载为复数类的成员函数。 #include<iostream> using namespace std; class complex//复数类声明 { public://外部接口 complex(double r=0.0,double i=0.0){real=r;imag=i;} //构造函数 complex operator + (complex c); //+重载为成员函数 complex operator -(complex c); //-重载为成员函数 void display();//输出复数 private://私有数据成员 double real;//复数实部 double imag;//复数虚部 }; complex complex::operator +(complex c2) //重载函数实现 { complex c; c.real=c2.real+real; c.imag=c2.imag+imag; return complex(c.real,c.imag); } complex complex::operator -(complex c2) //重载函数实现 { complex c; c.real=real-c2.real; c.imag=imag-c2.imag; return c; } void complex::display() { cout<<"("<<real<<","<<imag<<")"<<endl; } void main() //主函数 { complex c1(5,4),c2(2,10),c3; //声明复数类的对象 cout<<"c1="; c1.display(); cout<<"c2="; c2.display(); c3=c1-c2;//使用重载运算符完成复数减法 cout<<"c3=c1-c2="; c3.display(); c3=c1+c2;//使用重载运算符完成复数加法 cout<<"c3=c1+c2="; c3.display(); }运行结果:
c1=(5,4)
c2=(2,10)
c3=c1-c2=(3,-6)
c3=c1+c2=(7,14)
Press any key to continue
//运算符前置++和后置++重载为时钟类的成员函数。 #include<iostream> using namespace std; class Clock//时钟类声明 { public://外部接口 Clock(int NewH=0, int NewM=0, int NewS=0) {Hour = NewH; Minute = NewM; Second = NewS;} void SetTime(int NewH, int NewM,int NewS); void ShowTime(); Clock & operator ++(); //前置单目运算符重载 Clock operator ++(int); //后置单目运算符重载 private://私有数据成员 int Hour,Minute,Second; }; Clock& Clock::operator ++()//前置单目运算符重载函数 { Second++; if(Second>=60) { Second=Second-60; Minute++; if(Minute>=60) { Minute=Minute-60; Hour++; Hour=Hour%24; } } return *this; } //后置单目运算符重载 Clock Clock::operator ++(int) { //注意形参表中的整型参数 Clock old=*this; ++(*this); //调用了另一个成员函数 return old; } void Clock ::SetTime(int NewH, int NewM,int NewS) { Hour=NewH; Minute=NewM; Second=NewS; } void Clock ::ShowTime() { cout<<Hour<<":"<<Minute<<":"<<Second<<endl; } void main() { Clock myClock(23,59,59); cout<<"First time output:"; myClock.ShowTime(); cout<<"Show myClock++:"; (myClock++).ShowTime(); cout<<"Show ++myClock:"; (++myClock).ShowTime(); }运行结果:
First time output:23:59:59
Show myClock++:23:59:59
Show ++myClock:0:0:1
Press any key to continue
运算符友元函数的设计
运算符 定义式 等价式
双目运算符@ oprd1 @oprd2 operator @(oprd1,oprd2)
前置单目@ @ oprd operator @(oprd)
后置单目@ oprd@operator @(oprd,0 )
如果需要重载一个运算符,使之能够用于在类外操作某类对象的私有成员,可以此将运算符重载为该类的友元函数。
函数的形参代表了依自左至右次序排列的各操作数。
为了区分前置后置运算,后置单目运算符++和--的重载函数,其形参表中要增加一个int,但不必写形参名。
//将+、-(双目)重载为复数类的友元函数。 #include<iostream> using namespace std; class complex//复数类声明 { public://外部接口 complex(double r=0.0,double i=0.0){real=r;imag=i;} //构造函数 friend complex operator +(complex c1,complex c2);//运算符+重载为友元函数 friend complex operator -(complex c1,complex c2);//运算符-重载为友元函数 void display();//输出复数 private://私有数据成员 double real;//复数实部 double imag;//复数虚部 }; //运算符重载友元函数实现 complex operator +(complex c1,complex c2) { return complex(c2.real+c1.real, c2.imag+c1.imag); } //运算符重载友元函数实现 complex operator -(complex c1,complex c2) { return complex(c1.real-c2.real, c1.imag-c2.imag); } void complex::display() { cout<<"("<<real<<","<<imag<<")"<<endl; } void main() //主函数 { complex c1(5,4),c2(2,10),c3; //声明复数类的对象 cout<<"c1="; c1.display(); cout<<"c2="; c2.display(); c3=c1-c2;//使用重载运算符完成复数减法 cout<<"c3=c1-c2="; c3.display(); c3=c1+c2;//使用重载运算符完成复数加法 cout<<"c3=c1+c2="; c3.display(); }运算符重载的设计原则
运算符是: 建议设计为:
全部单目运算符 非静态成员函数
= () [ ] -> * 必须是非静态成员函数
+= -= *= /= %= 非静态成员函数
<<= >>= ^= |= &= 非静态成员函数
双目运算符 友元函数
>> << 必须是友元函数
虚函数 成员函数
若允许链式运算 该函数应返回本类型的引用
若定义了+ -* / % 千万别忘了定义=
对于任何形参,若仅读取值而不修改,则应设为const &;
函数的返回值的类型,可以是参数对象的类型(如+),也可以异于参数对象(如<=);
所有的赋值运算符皆可以改变左值。为满足链式运算要求,函数应返回同类型的非常引用。(记住:编译器是自左向右扫描表达式的,但却是从右向左来处理的。)
对于逻辑运算符,应返回bool类型,至少是int型;
对于自增自减运算符,可以返回本类的对象,亦可处理成返回是否继续迭代的逻辑值;
下标运算符必须是成员函数,单个形参,返回是引用;
()运算符是唯一允许带任意个参数的函数,必须设计为成员函数;
指针运算符->最神奇也最复杂,不但重载了,甚至演化为类,作用于容器,叫作迭代子。
4)虚函数
虚函数是动态绑定的技术基础。
是非静态的成员函数。
在类的声明中,在函数原型之前写virtual。
virtual只用来说明类声明中的原型,不能用在函数实现时。
具有继承性,基类中声明了虚函数,派生类中无论是否说明,同原型函数都自动为虚函数。
本质:不是重载(overload)而是覆盖(override)。
调用方式:通过基类指针或引用,执行时会根据指针指向的对象的类型,决定调用哪个类的函数。
虚函数的实现机制
编译器发现某类含有虚函数,则对其生成的对象悄悄地加入一个void 型的指向指针的指针vptr,并让其指向一个虚函数表vtable(其实是个指针数组),每个表项是一个虚函数名,排列次序按虚函数声明的次序排列。
在类族中,无论是基类还是派生类,都拥有各自的vptr和vtable。相同类型所生成的对象拥有相同的vtable。
派生类新增的虚函数依次排在后面。当然,派生类的vtable表项中放的是新的覆盖函数的首址。
动态多态的前提
(缺一不可)
必须有继承产生的类族;
必须是公有继承(类型兼容);
派生类的成员函数要重写该虚函数;
基类的某成员函数使用了virtual;
派生类的对象要使用指针或引用来调用该虚函数;
虚析构函数
为何需要虚析构函数?
避免析构对象不彻底。
何时需要虚析构函数?
当一个类含有虚函数时。
因为,当你打算用基类指针(或引用)删除子类对象,由于切割现象,会只释放基类部分,这会遗留内存垃圾。让基类的析构函数成为虚函数,则会彻底释放内存。
10. 局部类,内嵌类,抽象类
详见:/article/7786086.html
相关文章推荐
- C++ 点滴积累(5)
- C++ 点滴积累(3)
- C和C++的点滴积累(1)
- C++点滴积累
- C++ 点滴积累(2)
- C++ 点滴积累(1)
- C和C++的点滴积累(1)
- C++ 点滴积累(5)
- C++主题年技巧积累#1——UltraEdit的代码美化
- C++ 积累
- C++点滴
- JSF点滴积累--DataModel类
- 点滴积累【JS】
- C++基础知识积累
- OpenGL——点滴积累
- C++技术点积累(3)——对象初始化列表、运算符重载
- C++主题年技巧积累#2——我被static撞了一下腰
- C++技术点积累(6)——异常处理、输入和输出流
- JSF点滴积累-Overriding The JSF Renderers
- C++代码点滴