C/C++/OOP常见BUG
C语言是一种面向过程的计算机程序设计语言,它是目前众多计算机语言中举世公认的优秀的结构化程序设计语言之一。C++是在C语言的基础上发展而来的,它实现了由面向过程到面向对象的转变,是一种全面支持面向对象的程序设计方法。
目前C/C++程序设计方面的教程很多,但大多数是从语法、编程技巧、算法等角度进行组织和编写。在实际的软件系统开发过程中,许多刚涉足编程工作的程序员编写的代码往往质量不高,程序中往往隐藏着一些问题和错误,因程序员缺乏编程和调试经验而难以发现,给程序设计语言的学习和软件系统的开发造成了很大障碍。本文借鉴国内外的相关书籍、学术论文、网站论坛等文献资料,结合软件开发中经常遇到的实际问题和笔者长期从事软件系统开发的经验,从用户使用的角度出发,对C/C++编程中容易产生错误的知识点进行解释,对程序中常见的错误进行解析,以帮助他们尽快掌握C/C++编程技术,避免程序中的错误,提高代码质量,尽快成长为经验丰富的程序员。
本文内容按照C、C++和面向对象程序设计的顺序进行组织,共包括三部分,分别为:
u
第一部分 C语言编程常见BUG
本部分主要包括初学者常见问题、基本数据类型、存储类、运算符、流程控制、函数、C语言预处理程序、指针和数组、结构和联合、输入和输出、文件操作等内容。
u
第二部分 C++编程常见BUG
本部分主要包括命名空间、C++语言的输入输出、动态内存的分配与释放、引用、const修饰符、字符串、C++语言中函数的新特性等内容。
u
第三部分 面向对象程序设计编程常见BUG
本部分主要包括类与对象、友元、继承机制、多态和虚函数等内容。
本文针对C/C++编程中的常见错误,列举大量实例,并进行解析,以提高实用性,使读者容易理解,快速掌握。每个例子都给出了题目要求、错误代码、编译结果、问题分析、正确代码及其运行结果。其中在编译结果中给出了编译器提供的错误和警告信息。读者可以根据这些信息判断问题所在。每章后都有练习题,以帮助读者进一步巩固知识,增强效果。本文所附光盘中包括书中所有例题的源代码,课后练习的源代码及其答案的源代码。本文采用Visual C++
6.0作为编程和调试环境。
本文引用了大量书籍和文献资料,在此,向被引用文献的作者表示衷心的感谢,向为本文的编写和出版工作给予帮助的所有人士表示诚挚的敬意!
目 录
问题26 忽视了while和do-while语句在细节上的区别
问题86 派生类中由基类说明的数据成员应由基类的构造函数初始化
问题104 派生类必须实现所有纯虚函数才是具体类,否则仍是抽象类
本部分主要包括初学者常见问题、基本数据类型、存储类、运算符、流程控制、函数、C语言预处理程序、指针和数组、结构和联合、输入和输出、文件操作等内容。
初学编程的人往往会犯一些简单的错误。这些错误看上去不值一提,却是每个程序员成功之路上的拦路虎。这些常见的错误有:字母大小写混淆、容易混淆的字符(如逗号与分号;单引号与双引号;数字‘1’、大写字母‘I’与小写字母‘l’; 数字‘0’、大写字母‘O’与小写字母‘o’)、在代码中使用了中文字符、丢失或多余的分号、丢失或多余的大括号、混乱的缩进和对齐等。出现这些错误的根本原因在于编程和调试太少,对程序设计语言的基本语法掌握不够。多编写程序,多调试程序,自己改正程序中的错误,熟练之后就会避免再犯这些简单的错误。
[例1.1] 已知商品单价和数量,求总价。
[错误代码]
1 |
#include <stdio.h> |
2 |
void main( ) |
3 |
{ |
4 |
int count = 5; //数量 |
5 |
float price = 25; //单价 |
6 |
float total = price * Count; //计算总价 |
7 |
Printf("总价为% |
8 |
} |
[编译结果]
e:\code\1_1.cpp(6)
: error C2065: 'Count' : undeclared identifier
e:\code\1_1.cpp(7)
: error C2065: 'Printf' : undeclared identifier
[问题分析]
上面的编译结果给出了错误信息。双击某一错误信息行,该错误信息行会加亮显示,并在程序出现错误的行前面用一个箭头加以指示。第1、2条错误信息分别指出‘Count’、‘Printf’未定义。仔细对照程序,发现程序中如下语句定义了变量‘count’:
int count = 5;
首字母是小写;而
float total = price *
Count;
Printf("总价为%
这几两句代码中用到的‘Count’和‘Printf’的首字母写成了大写。C语言中是严格区分大小写的,所以编译器认为‘Count’未定义。
‘printf’是产生格式化输出的函数(定义在
stdio.h 中)。程序中把‘printf’误写成了‘Printf’,由于相同的原因,编译器也认为‘Printf’ 未定义。其他关键字如‘if’、‘for’等如果拼写错误或大小写错误也会报同样的错误信息。
[正确代码]
代码修改如下:
6 |
float total = price
* count; |
7 |
printf("总价为% |
C语言中是严格区分大小写的,只要定义变量的地方和使用这个变量的地方写法一致,就不会出现上面的问题。
[运行结果]
总价为125.00
[小提示]
如果程序编译不通过,编译器会给出错误信息。双击某一错误信息行,该错误信息行会加亮显示,并在程序出现错误的行前面用一个箭头加以指示。有时候,程序中的一个错误会导致多行错误信息,因此,常常修改一条错误后,再重新编译,直到没有错误为止。
有一些字符的外形看起来很相似,初学C语言编程的人很容易混淆。如逗号与分号;单引号与双引号;数字‘1’、大写字母‘I’与小写字母‘l’;数字‘0’、大写字母‘O’与小写字母‘o’;等等。
[例1.2] 已知学生数学、语文、英语三门课的成绩,求总分和平均分。
[错误代码]
1 |
#include
<stdio.h> |
2 |
void
main( ) |
3 |
{ |
4 |
float math = 79; |
5 |
f1oat chinese = 80; |
6 |
float english = |
7 |
|
8 |
float total =
math+chinese+engIish;
//求总分 |
9 |
fIoat average =
total/3,
//求平均分 |
10 |
printf('总分为% |
11 |
} |
[编译结果]
E:\Code\1_2.cpp(5)
: error C2065: 'f1oat' : undeclared identifier
E:\Code\1_2.cpp(5)
: error C2146: syntax error : missing ';' before identifier 'chinese'
E:\Code\1_2.cpp(5)
: error C2065: 'chinese' : undeclared identifier
E:\Code\1_2.cpp(8)
: error C2065: 'engIish' : undeclared identifier
E:\Code\1_2.cpp(9)
: error C2065: 'fIoat' : undeclared identifier
E:\Code\1_2.cpp(9)
: error C2146: syntax error : missing ';' before identifier 'average'
E:\Code\1_2.cpp(9)
: error C2065: 'average' : undeclared identifier
E:\Code\1_2.cpp(9)
: warning C4244: '=' : conversion from 'float' to 'int', possible loss of data
E:\Code\1_2.cpp(10)
: error C2001: newline in constant
E:\Code\1_2.cpp(10)
: error C2015: too many characters in constant
E:\Code\1_2.cpp(11)
: error C2143: syntax error : missing ')' before '}'
E:\Code\1_2.cpp(11)
: error C2143: syntax error : missing ';' before '}'
[问题分析]
让我们逐行分析这些错误提示。
“error C2065: 'f1oat' : undeclared identifier”:f1oat chinese = 80这一句里的“f1oat”中字母‘l’被写成了数字‘
“syntax error : missing ';' before identifier
'chinese'”:同上
“error C2065: 'chinese' : undeclared
identifier”:同上
“error C2065: 'engIish' : undeclared
identifier”: 'engIish'中字母‘l’被写成了大写字母‘I’。
“error C2065: 'fIoat' : undeclared identifier”: fIoat
average = total/3这一句里的“fIoat”中字母‘l’被写成了大写字母‘I’。
“error C2001: newline in constant”: fIoat
average = total/3, 这一句最后的分号被写成逗号。
最后3条错误提示是由于printf('总分为%.
以上错误都被更正之后,编译通过,运行结果如下:
总分为167.00,平均分为55.67
奇怪,数学、语文、英语三门课的成绩分别为79、80、81,总分应该为240.00,平均分应该为80.00,运行结果和预期不一致。到底哪里出错呢?原来是float english =
[正确代码]
代码修改如下:
5 |
float
chinese = 80; |
6 |
float english = 81; |
7 |
|
8 |
float total =
math+chinese+english; |
9 |
float
average = total/3; |
10 |
printf("总分为% |
[运行结果]
正确的运行结果如下:
总分为240.00,平均分为80.00
在录入程序时,如果忘了把输入法切换到英文状态,就会不慎录入中文字符,如逗号、引号和分号,因为中文输入法与英文输入法下键入符号的编码不同,从而导致一系列编译错误。
[例1.3] 已知矩形的长和宽,求该矩形的面积。
[错误代码]
1 |
#include
<stdio.h> |
2 |
void
main( ) |
3 |
{ |
4 |
int length = 5,int width = 6; |
5 |
int area = length *
width; |
6 |
printf(“面积为%d\n",area); |
7 |
} |
[编译结果]
e:\code\1_3.cpp(4)
: error C2018: unknown character '0xa3'
e:\code\1_3.cpp(4)
: error C2018: unknown character '0xac'
e:\code\1_3.cpp(4)
: error C2144: syntax error : missing ';' before type 'int'
e:\code\1_3.cpp(5)
: error C2018: unknown character '0xa3'
e:\code\1_3.cpp(5)
: error C2018: unknown character '0xbb'
e:\code\1_3.cpp(6)
: error C2146: syntax error : missing ';' before identifier 'printf'
e:\code\1_3.cpp(6)
: error C2018: unknown character '0xa1'
e:\code\1_3.cpp(6)
: error C2018: unknown character '0xb0'
e:\code\1_3.cpp(6)
: error C2018: unknown character '0xc3'
e:\code\1_3.cpp(6)
: error C2018: unknown character '0xe6'
e:\code\1_3.cpp(6)
: error C2018: unknown character '0xbb'
e:\code\1_3.cpp(6)
: error C2018: unknown character '0xfd'
e:\code\1_3.cpp(6)
: error C2018: unknown character '0xce'
e:\code\1_3.cpp(6)
: error C2018: unknown character '0xaa'
e:\code\1_3.cpp(6)
: error C2143: syntax error : missing ')' before '%'
e:\code\1_3.cpp(6)
: error C2660: 'printf' : function does not take 0 parameters
e:\code\1_3.cpp(6)
: error C2017: illegal escape sequence
e:\code\1_3.cpp(6)
: error C2065: 'd' : undeclared identifier
e:\code\1_3.cpp(6)
: error C2001: newline in constant
e:\code\1_3.cpp(6)
: error C2146: syntax error : missing ';' before identifier 'n'
e:\code\1_3.cpp(6)
: error C2065: 'n' : undeclared identifier
e:\code\1_3.cpp(6)
: error C2143: syntax error : missing ';' before 'string'
e:\code\1_3.cpp(7)
: error C2143: syntax error : missing ';' before '}'
[问题分析]
上面的编译结果给出了错误提示。令人吃惊的是短短的几行代码居然报了23个错误。不必担心,真正的错误只有3处:int length = 5之后的中文逗号;int area = length * width之后的中文分号;“printf(”与“面积”之间的中文引号。因为一个中文字符的内码占用两个字节,所以会报告类似的错误提示:
e:\code\1_3.cpp(4)
: error C2018: unknown character '0xa3'
e:\code\1_3.cpp(4)
: error C2018: unknown character '0xac'
凡是有类似错误提示的都是因为代码里用了中文符号。
[正确代码]
只要把中文符号改为对应的英文符号即可。
4 |
int length = 5, int width = 6; |
5 |
int area = length *
width; |
6 |
printf("面积为%d\n",area); |
[运行结果]
面积为30
初学者还经常丢掉分号,或写了多余的分号。这将导致一系列编译错误。
[例1.4] 计算从1到10的10个整数之和。
[错误代码]
1 |
#include
<stdio.h> |
2 |
void
main( ) |
3 |
{ |
4 |
int sum = 0; |
5 |
for(int
i=1;i<=10;i++); |
6 |
sum += i; |
7 |
printf("sum=%d\n",sum) |
8 |
} |
[编译结果]
E:\Code\1_4.cpp(8)
: error C2143: syntax error : missing ';' before '}'
[问题分析]
上面的编译结果给出了错误提示。“missing ';' before '}'”告诉我们printf("sum=%d\n",sum)后面少写了一个分号。在此处添加分号,则编译正常,运行结果为:
sum=11
从1到10的10个整数之和应改为55,而不是11。错误原因是for(int i=1;i<=10;i++);这一行最后多了一个分号,导致sum += i并不是在循环体里面循环执行,而是等循环结束之后才执行。因而实际结果为11。
[正确代码]
代码修改如下:
5 |
for(int
i=1;i<=10;i++) //此处删除分号‘;’ |
7 |
printf("sum=%d\n",sum); //添加分号‘;’ |
[运行结果]
sum=55
当程序代码中包括for、if、while、switch等语句时,程序中就会出现较多的‘{’和‘}’。它们是成对出现的。如果丢失了‘{’或‘}’,会导致程序出错。而且编译程序给出的错误信息往往不一定准确指明错误所在的位置,导致调试时遇到困难。
[例1.5] 依次读入10个学生的成绩,判断成绩等级(优、良、中、及格、不及格),统计各等级的人数,并输出。
[错误代码]
1 |
#include <stdio.h> |
2 |
void main( ) |
3 |
{ |
4 |
int excellent=0, good=0, middle=0, pass=0, fail=0; |
5 |
for(int i=0;i<10;i++) |
6 |
{ |
7 |
float score; |
8 |
scanf("%f",&score); //输入分数 |
9 |
if(score<=100 && score>=90) //优 |
10 |
{ |
11 |
excellent++; |
12 |
printf("优\n"); |
13 |
} |
14 |
else if(score<90 && score>=80) //良 |
15 |
good++; |
16 |
printf("良\n"); |
17 |
} |
18 |
else if(score<80 && score>=70) //中 |
19 |
{ |
20 |
middle++; |
21 |
printf("中\n"); |
22 |
} |
23 |
else if(score<70 && score>=60) //及格 |
24 |
{ |
25 |
pass++; |
26 |
printf("及格\n"); |
27 |
} |
28 |
else if(score<60 && score>=0) //不及格 |
29 |
{ |
30 |
fail++; |
31 |
printf("不及格\n"); |
32 |
} |
33 |
printf("统计结果:\n优:%d\n", excellent); |
34 |
printf("良:%d\n", good); |
35 |
printf("中:%d\n", middle); |
36 |
printf("及格:%d\n", pass); |
37 |
printf("不及格:%d\n", fail); |
38 |
} |
[编译结果]
E:\Code\1_5.cpp(18)
: error C2181: illegal else without matching if
E:\Code\1_5.cpp(18)
: error C2065: 'score' : undeclared identifier
[问题分析]
第1条错误信息是“legal else without matching if”,意思是else没有与它匹配的if。双击这条错误信息,编辑区焦点自动移到下面这一句代码:
else if(score<80 && score>=70)
但是前面明明有else if(score<90 &&
score>=80),为什么还报这个错呢?原来,由于else if(score<90 &&
score>=80)后面丢失了一个‘{’, 编译器认为printf("良\n")这一句后面的‘}’不是与else if匹配的,而是与for匹配的,导致错误。改正这一句后再编译,仍有错误,错误信息为:
E:\Code\1_5.cpp(40)
: fatal error C1004: unexpected end of file found
Error
executing cl.exe.
双击这条错误信息行,该错误信息行会加亮显示,并在程序最后一行前面用一个箭头加以指示。这更加令人感到困惑,到底为什么呢?原来,因为printf("优:%d\n", excellent)前面丢失了一个‘}’, 编译器认为代码最后一行的‘}’是与for匹配的,那么main函数的结束‘}’就没有了,所以编译器会报告这样的错误信息。
[正确代码]
1 |
#include
<stdio.h> |
2 |
void
main( ) |
3 |
{ |
4 |
int excellent=0,
good=0, middle=0, pass=0, fail=0; |
5 |
for(int
i=0;i<10;i++) |
6 |
{ |
7 |
float score; |
8 |
scanf("%f",&score); |
9 |
if(score<=100 && score>=90) //优 |
10 |
{ |
11 |
excellent++; |
12 |
printf("优\n"); |
13 |
} |
14 |
else if(score<90 && score>=80) //良 |
15 |
{ |
16 |
good++; |
17 |
printf("良\n"); |
18 |
} |
19 |
else if(score<80 && score>=70) //中 |
20 |
{ |
21 |
middle++; |
22 |
printf("中\n"); |
23 |
} |
24 |
else if(score<70 && score>=60) //及格 |
25 |
{ |
26 |
pass++; |
27 |
printf("及格\n"); |
28 |
} |
29 |
else if(score<60 && score>=0) //不及格 |
30 |
{ |
31 |
fail++; |
32 |
printf("不及格\n"); |
33 |
} |
34 |
} |
35 |
printf("统计结果:\n优:%d\n", excellent); |
36 |
printf("良:%d\n", good); |
37 |
printf("中:%d\n", middle); |
38 |
printf("及格:%d\n", pass); |
39 |
printf("不及格:%d\n", fail); |
40 |
} |
[运行结果]
100
优
90
优
80
良
76
中
56
不及格
34
不及格
67
及格
78
中
89
良
91
优
统计结果:
优:3
良:2
中:2
及格:1
不及格:2
一个程序员需要有良好的编程风格,例如代码行的缩进和对齐。初学者往往忽视这一点,或者认为这无关紧要。实际上混乱的缩进和对齐不仅影响代码的美观,还使代码变得难以理解,降低了工作效率,影响了团队合作。它还降低了代码的质量,增加了代码出错的概率,使程序员不容易找出代码中的错误。
[例1.6] 要求在屏幕上输出如下图形:
#
**
###
****
#####
******
#######
********
#########
[错误代码]
1 |
#include <stdio.h> |
2 |
void main( ) |
3 |
{ for(int i=1;i<10;i++) |
4 |
{ |
5 |
for(int j=0;j<i;j++) |
6 |
if((i/2)*2==i) |
7 |
printf("*"); else |
8 |
printf("#"); |
9 |
} |
10 |
printf("\n"); |
11 |
} |
12 |
} |
[编译结果]
E:\Code\1_6.cpp(12)
: error C2143: syntax error : missing ';' before '}'
E:\Code\1_6.cpp(12)
: error C2143: syntax error : missing ';' before '}'
E:\Code\1_6.cpp(12)
: error C2143: syntax error : missing ';' before '}'
[问题分析]
这段代码混乱不堪,代码没有合理地缩进,成对的大括号也没有对齐。事实上很难看懂。编译产生的错误提示也没有准确指出错误所在。实际上如果使代码合理地缩进和对齐,就可以很容易看出,for(int j=0;j<i;j++)之后缺少了一个‘}’。
[正确代码]
把混乱的代码进行整理,在for(int j=0;j<i;j++)之后加了一个‘}’。
1 |
#include
<stdio.h> |
2 |
void
main( ) |
3 |
{ |
4 |
for(int i=1;i<10;i++) |
5 |
{ |
6 |
for(int j=0;j<i;j++) |
7 |
{ |
8 |
if((i/2)*2==i) |
9 |
printf("*");
|
10 |
else |
11 |
printf("#"); |
12 |
} |
13 |
printf("\n"); |
14 |
} |
15 |
} |
[运行结果]
同题目要求。
[小提示]
如何快速地规范代码缩进格式呢?必须一行一行地改吗?其实,只要选中需要规范的代码,按Alt+F8即可。
请改正如下程序中的错误。
1) 用#号输出字母E的图案。
1 |
#include <stdio.h> |
2 |
void main( ) |
3 |
{ |
4 |
Printf(" ####/n"); |
5 |
printf(" #\n"); |
6 |
printf(" ####\n"); |
7 |
prlntf(" # \n"); |
8 |
printf(" ####\n"); |
9 |
} |
2) 一个整数,它加上100后是一个完全平方数,再加上168又是一个完全平方数,请问该数是多少?
1 |
#include <stdio.h> |
2 |
#include "math.h" |
3 |
void main( ) |
4 |
{ |
5 |
long int i,x,y; |
6 |
for(i=1;i<100000;i++); |
7 |
{ |
8 |
x=sqrt(i+100); |
9 |
y=sqrt(i+168); |
10 |
if(x*x==i+100 && y*y==i+168); |
11 |
printf("%ld\n",i); |
12 |
} |
13 |
} |
3) 有1、2、3、4四个数字,能组成多少个互不相同且无重复数字的三位数?都是多少?
1 |
#include <stdio.h> |
2 |
void main( ) |
3 |
{ |
4 |
int i,j,k; |
5 |
printf("\n"); |
6 |
for(i=1;i<5;i++) |
7 |
for(j=1;j<5;j++) |
8 |
for (k=1;k<5;k++) |
9 |
{ |
10 |
//确保i、j、k三位互不相同 |
11 |
if (i!=k && i!=j && j!=k) |
12 |
printf("%d,%d,%d\n",i,j,k); |
13 |
} |
14 |
} |
15 |
} |
16 |
} |
在C语言中,数据类型可分为:基本数据类型,构造数据类型,指针类型三大类。如下:
基本数据类型最主要的特点是,其值不可以再分解为其它类型,包括整型、浮点型、字符型、空类型、枚举类型等。其中整型又分为短整型(short)、整型(int)、长整型(long),浮点型又分为单精度浮点型(float)和双精度浮点型(double)。
构造数据类型是根据已定义的一个或多个数据类型用构造的方法来定义的,一个构造类型的值可以分解成若干个“成员”,每个“成员”都是一个基本数据类型或者一个构造类型。在C语言中,构造类型有数组类型、结构类型(struct)、联合类型(union)等几种。
指针类型用来表示某个量在内存中的地址。在计算机系统中每一个数据均需要占用一定的内存空间,而每段空间均有唯一的地址与之对应,因此在计算机系统中任意数据均有确定的地址与之对应。C语言中,为了描述数据存放的地址信息,引入指针类型。虽然指针的取值类似于整型量,但这是两个类型完全不同的量,因此不能混为一谈。
在C语言中,数据的基本表现形式是常量和变量。在程序执行过程中,其值不发生改变的量称为常量,它可以不经说明而直接引用。常量可分为整型常量、浮点常量、字符常量、枚举常量等。有时候,可以用一个符号名来代表一个常量,称为符号常量。例如在程序中经常使用圆周率3.14,可以用符号PI来表示,这样在每个用到它的地方只需用PI来代替3.14。如果要把3.14改为3.14159265,只需修改一个地方,而不是每个用到圆周率的地方。
在程序执行过程中,取值可变的量称为变量。在程序中,变量则必须先说明后使用。变量可以分为整型变量、浮点变量、字符变量、枚举变量等。使用变量时,需要注意区别变量名和变量值。变量代表内存中具有特定属性的一个和几个存储单元,它用来存储数据,也就是存储变量的量。变量应该有一个变量名,以便于被引用。变量名必须符合C语言对标识符的规定。
1. 整型数据
(1)整型常量
整型常量即常整数。在C语言中,整型常量通常以十进制整数、八进制整数、十六进制整数来表示。在程序中根据前缀来区分各种不同进制的数。
十进制整数没有前缀,其数码为0~9。25、-90、65535、1398都是合法的十进制整数,而029(不能有前导0)、23D((含有非十进制数码)则不是合法的十进制整数。
八进制整数必须以0开头,即以0作为八进制数的前缀。数码取值为0~7。八进制数通常是无符号数。015(十进制为13)、0101(十进制为65)、0177777(十进制为65535) 都是合法的八进制数。而26(无前缀0)、
十六进制整数的前缀为0X或0x。其数码取值为0~9,A~F或a~f。0X2B(十进制为43)、0XA1(十进制为161)、0XFFFF(十进制为65535)是合法的十六进制整常数,而
在程序中,以上整数的三种表示形式都是合法、有效的。例如,16、020、0x10都表示十进制数16。
(2)整型变量
在C语言中有三种整型变量:整型(int),短整型(short int或short),长整型(long int或long)。ANSI C标准没有具体规定以上各类数据所占内存的字节数,只要求long型数据长度不短于int型,int型数据长度不短于short型。具体由各计算机系统自行决定。在Turbo C中,int型和short型数据都是2个字节,即16位二进制。long型数据是4个字节,即32位二进制。而Visual C++中,short型数据是2个字节,即16位二进制。而int型和long型数据是4个字节,即32位二进制。
一般情况下,整型变量是有符号的,即存储单元的最高位用来表示符号(0为正,1为负)。例如,一个short型数据被分配2个字节,即16位二进制,最高位存储符号,用来存储数据的是15位,存放的数据范围是-32768 ~ 32767。在实际应用中,变量的符号通常是正的,如分数、价格、年龄等。为充分利用变量的数值范围,可以将变量定义为“无符号”类型。只需要在short、int、long之前加上unsigned修饰符即可,如unsigned short、unsigned int、unsigned long. 这样,对于unsigned short来说,用来存储数据的是16位,存放的数据范围是0 ~ 65535,使可以存放正数的范围扩大了一倍。修饰符signed表示有符号数,实际上可以省略。归纳起来,在C语言中可以定义六种整型变量,即:
有符号整型 [signed] int
无符号整型 unsigned int
有符号短整型 [signed] short [int]
无符号短整型 unsigned short [int]
有符号长整型 [signed] long [int]
无符号长整型 unsigned long [int]
表2.1整数类型的数据取值范围
类型 |
Turbo C |
Visual
C++ |
||
字节数 |
数值范围 |
字节数 |
数值范围 |
|
[signed] int |
2 |
-32768 ~ 32767 |
4 |
-2147483648~2147483647 |
unsigned int |
2 |
0 ~ 65535 |
4 |
0 ~ 4294967295 |
[signed] short
[int] |
2 |
-32768 ~ 32767 |
2 |
-32768 ~ 32767 |
unsigned short
[int] |
2 |
0 ~ 65535 |
2 |
0 ~ 65535 |
[signed] long
[int] |
4 |
-2147483648~2147483647 |
4 |
-2147483648~2147483647 |
unsigned long
[int] |
4 |
0 ~ 4294967295 |
4 |
0 ~ 4294967295 |
变量说明的一般形式为: 类型说明符 变量名标识符,变量名标识符,...; 例如:
int a,b,c; (a,b,c为整型变量)
long m,n; (m,n为长整型变量)
unsigned int p,q; (p,q为无符号整型变量)
在说明变量时,应注意:
允许在一个类型说明符后,说明多个相同类型的变量。各变量名之间用逗号间隔。类型说明符与变量名之间至少用一个空格间隔。
最后一个变量名之后必须以“;”号结尾。
变量说明必须放在变量使用之前。
2. 浮点型数据
(1)浮点型常量
在C语言中,浮点数就是实数。实数有二种形式:
十进制小数形式:由数码0~ 9和小数点组成。如3.2,0.35,-321.897。
指数形式:类似2.5×104的数称为指数形式。计算机中用字母e或E代表以10为底的指数,如2.5E4代表2.5×104。一个浮点数在以指数形式输出时,通常采用“规范化的指数形式”,即在字母e或E之前的小数部分中,小数点左边有且仅有一位非零的数字。
(2)浮点型变量
在C语言中,浮点型变量分为两类:
单精度型:类型说明符为float,在Turbo C中占4个字节(32位)内存空间,其数值范围为3.4E-38~3.4E+38,可提供七位有效数字。例如: float x,y;
双精度型:类型说明符为double,在Turbo C中占8 个字节(64位)内存空间,其数值范围为1.7E-308~1.7E+308,可提供16位有效数字。例如:double a,b,c;
3. 字符型数据
(1)字符常量
字符常量是用单引号括起来的一个字符。例如'a'、'b'、'$'、'?'都是合法的字符常量。
另外,C语言中允许使用转义字符。转义字符是一种特殊的字符常量,以反斜杠"\"开头,后跟一个或几个字符。“转义字符”的意思是把反斜杠"\"后面的字符转换成另外的含意。常用的转义字符见表2.2。
表2.2常用的转义字符及其作用
字符形式 |
含义 |
ASCII代码 |
\n |
换行,把当前位置移到下一行开头 |
10 |
\t |
水平制表(跳到下一制表位置) |
9 |
\b |
退格 |
8 |
\r |
回车 |
13 |
\f |
换页 |
12 |
\a |
鸣铃 |
7 |
\\ |
反斜杠"\" |
92 |
\' |
单引号 |
39 |
\" |
双引号 |
34 |
\ddd |
1~3位八进制数所代表的字符 |
|
\xhh |
1~2位十六进制数所代表的字符 |
|
广义地讲,C语言字符集中的任何一个字符均可用转义字符来表示。上表中的ddd和hh分别为八进制和十六进制的ASCII代码。
(2)字符变量
字符变量的类型说明符是char,其取值是字符常量,即单个字符。每个字符变量被分配一个字节的内存空间,因此只能存放一个字符。字符值是以ASCII码的形式存放在变量的内存单元之中的。字符变量说明的格式和书写规则都与整型变量相同。例如:char a,b;
(3)字符串常量
字符串常量是由一对双引号括起的字符序列。例如: "CHINA" ,"Hello","12.5" 等。在C语言中没有相应的字符串变量,可以用一个字符数组来存放一个字符串常量。字符串常量和字符常量是不同的量。二者有以下区别:
字符常量由单引号括起来,字符串常量由双引号括起来。
字符常量只能是单个字符,字符串常量则可以含一个或多个字符。
字符常量占一个字节的内存空间,而字符串常量占的内存字节数等于字符串中字符个数加1。这是因为需要在字符串的后面增加字符串结束标志,即字符'\0'(ASCII码为0)。
[例2.1] 在屏幕上输出两个整型常量(例如10和17)的和。
[错误代码]
1 |
#include <stdio.h> |
2 |
void main( ) |
3 |
{ |
4 |
int a = 10; |
5 |
int b = 017; |
6 |
int c = a + b; |
7 |
printf("a+b=%d\n",c); |
8 |
} |
[编译结果]
编译通过。但运行结果是:
a+b=25
[问题分析]
编译虽然通过了,但运行结果是a+b=25。按理说a+b=10+17=27,怎么会等于25呢?
实际上,如果一个整型常量的第一个字符是数字0,那么该常量将被视作八进制数。例如17与017的含义是不同的。前者只是十进制数字17,而后者是八进制数017,即十进制数字15。由于在int b = 017;里把17写成017,导致结果出错。
[正确代码]
代码改动如下:
5 |
int b = 17; //此处把017改成17 |
[运行结果]
a+b=27
[例2.2] 定义一个字符常量,赋值,并输出。
[错误代码]
1 |
#include <stdio.h> |
2 |
void main( ) |
3 |
{ |
4 |
char s; |
5 |
s = "a"; |
6 |
printf("s=%c\n",s); |
7 |
} |
[编译结果]
E:\Code\2_2.cpp(5)
: error C2440: '=' : cannot convert from 'char [2]' to 'char'
This
conversion requires a reinterpret_cast, a C-style cast or function-style cast
[问题分析]
char s;
s="a";
在这里就混淆了字符常量与字符串常量,字符常量是由一对单引号括起来的单个字符,字符串常量是一对双引号括起来的字符序列。C规定以'\0'作为字符串结束标志,它是由系统自动加上的,所以字符串“a”实际上包含两个字符:'a'和'\0',而把它赋给一个字符变量是不行的。
[正确代码]
代码改动如下:
5 |
s = 'a'; |
[运行结果]
s=a
[例2.3] 输入两个数,求二者的商,输出结果。
[错误代码]
1 |
#include <stdio.h> |
2 |
void main( ) |
3 |
{ |
4 |
float a, b; |
5 |
scanf("%f %f",&a, &b); |
6 |
if(b==0) \*判断分母是否为0*/ |
7 |
{ |
8 |
printf("分母为0!/n"); |
9 |
} |
10 |
else |
11 |
{ |
12 |
float c = a\b; |
13 |
printf("a/b=% |
14 |
} |
15 |
} |
[编译结果]
E:\Code\2_3.cpp(6)
: error C2017: illegal escape sequence
E:\Code\2_3.cpp(6)
: error C2018: unknown character '0xc5'
E:\Code\2_3.cpp(6)
: error C2018: unknown character '0xd0'
E:\Code\2_3.cpp(6)
: error C2018: unknown character '0xb6'
E:\Code\2_3.cpp(6)
: error C2018: unknown character '0xcf'
E:\Code\2_3.cpp(6)
: error C2018: unknown character '0xb7'
E:\Code\2_3.cpp(6)
: error C2018: unknown character '0xd6'
E:\Code\2_3.cpp(6)
: error C2018: unknown character '0xc4'
E:\Code\2_3.cpp(6)
: error C2018: unknown character '0xb8'
E:\Code\2_3.cpp(6)
: error C2018: unknown character '0xca'
E:\Code\2_3.cpp(6)
: error C2018: unknown character '0xc7'
E:\Code\2_3.cpp(6)
: error C2018: unknown character '0xb7'
E:\Code\2_3.cpp(6)
: error C2018: unknown character '0xf1'
E:\Code\2_3.cpp(6)
: error C2018: unknown character '0xce'
E:\Code\2_3.cpp(6)
: error C2018: unknown character '0xaa'
E:\Code\2_3.cpp(6)
: warning C4138: '*/' found outside of comment
E:\Code\2_3.cpp(6)
: error C2100: illegal indirection
E:\Code\2_3.cpp(6)
: error C2059: syntax error : '/'
E:\Code\2_3.cpp(7)
: error C2143: syntax error : missing ';' before '{'
E:\Code\2_3.cpp(10)
: error C2181: illegal else without matching if
E:\Code\2_3.cpp(12)
: error C2017: illegal escape sequence
E:\Code\2_3.cpp(12)
: error C2146: syntax error : missing ';' before identifier 'b'
[问题分析]
‘/’和‘\’这两个符号经常被混淆;注释对应的符号是‘/’,而转义字符是以‘\’开头,除号是‘/’。
在本例中,第6行代码居然报了18条错误信息!实际上这只是因为第6行代码中把表示注释的“/*”错误地写成了“\*”导致。因为把表示注释的“/*”错误地写成了“\*”,所以后面的“判断分母是否为0*/”就被认为是程序代码,而不是注释。因此才报了这么多错。第12行代码把除号“/”写成了“\”。重新编译则通过。运行,输入a和b,看到的运行结果如下:
5.2 2.0
a/b=2.60/nPress
any key to continue
从结果可以看出,第13行代码中把换行符“\n”错写为“/n”。
[正确代码]
代码改动如下:
6 |
if(b==0) /*判断分母是否为0*/ |
以及
12 |
float c = a/b; |
13 |
printf("a/b=% |
[运行结果]
5.2 2.0
a/b=2.60
Press any
key to continue
如果一个运算符两边的运算数类型不同,先要将其转换为相同的类型,即较低精度类型转换为较高精度类型,然后再参加运算。如两个float型数参加运算,虽然它们类型相同,但仍要先转成double型再进行运算,结果亦为double型。当运算符两边的运算数为不同类型时的转换,如一个long 型数据与一个int型数据一起运算,需要先将int型数据转换为long型,然后两者再进行运算,结果为long型。所有这些转换都是由系统自动进行的,但是,C语言也提供了以显式的形式强制转换类型的机制。
当较低精度类型的数据转换为较高精度类型时,一般只是形式上有所改变,而不影响数据的实质内容,而较高精度类型的数据转换为较低精度类型时则可能引起数据丢失。
当赋值运算符两边的运算对象类型不同时,将发生类型转换,转换的规则是:把赋值运算符右侧表达式的类型转换为左侧变量的类型。
整数相除会降低精度,丢失小数部分。可以在整数相除之前先做强制类型转换,以避免这种危险。
[例2.4] 已知一个整数,求它的倒数并输出。
[错误代码]
1 |
#include <stdio.h> |
2 |
void main( ) |
3 |
{ |
4 |
int x=3; |
5 |
float y = 1/x; |
6 |
printf("x的倒数为:% |
7 |
} |
[编译结果]
编译通过。
[问题分析]
虽然编译通过了,但运行结果为:
x的倒数为:0.00
实际上,3的倒数保留两位小数应该为0. 33。错误在哪里呢?因为x是整数3,1也是整数,则1/x是整数相除,结果为整数0,不会得到0. 33,进而会影响后边的计算结果。因此,可以把float y = 1/x;改成float y = 1/(float)x,或者改为float y = 1.0/x .当计算不同类型的数据时,一定要注意会不会出现引起错误的自动类型转换,建议最好加上强制转换。
[正确代码]
代码修改如下:
6 |
float y = 1/(float)x; //或者改为float y = 1.0/x; |
[运行结果]
x的倒数为:0.33
当较低精度类型的数据转换为较高精度类型时,一般只是形式上有所改变,而不影响数据的实质内容,而较高精度类型的数据转换为较低精度类型时则可能有些数据丢失。如int型数值赋给char型变量时,只保留其最低8位,高位部分舍弃。 而long型数据赋给int型变量时,将低16位值送给int型变量,而将高16 位截断舍弃。(这里假定int型占两个字节)。
[例2.5] 已知商品价格和折扣率,求折扣后的价格。
[错误代码]
1 |
#include <stdio.h> |
2 |
void main( ) |
3 |
{ |
4 |
float price = |
5 |
float discount = |
6 |
int newPrice = price*discount; //计算折扣价 |
7 |
printf("折扣后的价格为:%d\n",newPrice); |
8 |
} |
[编译结果]
e:\code\2_5.cpp(6)
: warning C4244: 'initializing' : conversion from 'float' to 'int', possible
loss of data
[问题分析]
编译没有错误,只有一个警告:conversion from 'float' to 'int', possible
loss of data.运行结果为:
折扣后的价格为:8
因为两个float值相乘,结果还是float型。将这个结果赋给一个整型变量,结果会舍弃高位部分。
[正确代码]
代码修改如下:
6 |
float newPrice = price*discount; //计算折扣价 |
7 |
printf("折扣后的价格为:% |
[运行结果]
折扣后的价格为:8.50
请改正如下程序中的错误。
1) 写一个函数strlen,返回给定字符串的长度。
1 |
#include <stdio.h> |
2 |
int strlen(char s[]) |
3 |
{ |
4 |
int i=0; |
5 |
while(s[i] != "\0") |
6 |
++i; |
7 |
return i; |
8 |
} |
9 |
|
10 |
void main( ) |
11 |
{ |
12 |
int len = strlen("Hello,world!"); |
13 |
printf("len = %d/n", len); |
14 |
} |
2) 有一分数序列:2/1,3/2,5/3,8/5,13/8,21/13...求出这个数列的前20项之和。(结果应该为32.660259,但如下程序的运行结果为21.000000,请改正其中的错误)
1 |
#include <stdio.h> |
2 |
void main( ) |
3 |
{ |
4 |
int number=20; |
5 |
int a=2,b=1; |
6 |
float s=0; |
7 |
for(int n=1;n<=number;n++) |
8 |
{ |
9 |
s=s+a/b; |
10 |
int t=a;a=a+b;b=t; |
11 |
} |
12 |
printf("sum is
% |
13 |
} |
在C语言中,每一个数据,不管是常量还是变量,都具有一定的数据类型。实际上,所有的变量不但具有一定的数据类型,还有一个存储类的问题。从编译程序的观点来看,一个变量名就等同于计算机中的某个物理存储单元,在该单元中,存储着用一串二进制数位代表的该变量的当前值。在计算机中,主要有两种单元:存储器单元和寄存器单元。变量存于存储器或寄存器就决定了它的存储类。变量的存储类确定了变量的作用域。
在C语言中有四种存储类:自动变量、寄存器变量、静态变量、外部变量,分别由auto、register、static、extern关键字来说明。
1. 自动变量
自动变量用关键字auto进行说明,其作用域被限制在该变量出现的那一块内。只要那一块或包含在那一块里的任何块被执行,该变量就存在而且可以被引用。如果程序离开了那一块,该变量就不存在。当不需要它们时,它们不占据任何存储空间。
每当一个变量在块内说明时,如果没有给予显式的存储类说明,则该变量就被看成是自动变量。大部分变量都属于这一种。所以说明自动变量的关键字auto是可以缺省的。自动变量可能通过以下方式加以说明:
auto int a;
auto int m = 33;
auto char ch;
……
2. 寄存器变量
在CPU内部的寄存器中进行操作比在存储器上执行起来要快得多。当把一个变量说明为寄存器变量后,编译程序将尽可能地为该变量分配CPU内部的寄存器作为变量的存储单元,以加快运行速度。如果寄存器已经分配完毕,就把该寄存器变量当作自动变量来处理,即放在存储器中;此时,它的作用等价于auto,也只能用于局部变量和函数的参量说明。寄存器变量用register关键字通过以下方式加以说明:
register int a;
register char ch;
……
实际上,根据具体的机器硬件,对寄存器变量存在一些限制。通常不建议使用它。
3. 静态变量
静态变量具有静态生存期,用static关键字通过以下方式加以说明:
static int a;
static int m = 33;
static char ch;
……
与自动变量一样,如果一个静态变量是在一个函数(或块)内加以说明,则该静态变量是“局部的”,即其作用域被限制在它出现的那一块内。但与自动变量不同的是,静态变量在函数退出时保持其数值。下次再调用这个函数时,静态变量仍包含上次函数退出时的值。
静态变量可以是内部的(即局部于一个函数或块),也可以是外部的。如果静态变量的说明出现于任何函数之前,则这些静态变量对该文件中的所有函数均是已知的,是外部静态变量,但在其他文件中则是未知的。外部静态变量提供了隐藏数据对象的方法。
4. 外部变量
与前三种不同,外部变量的作用域是全局的,而不是局部的。例如:
int x = 10;
void main( )
{
printf(“%d\n”,x);
}
虽然x在main函数之外定义,但在该函数内仍然是可以使用的。如果所用的局部变量和外部变量名字重复了,在发生冲突时,外部变量被屏蔽,局部变量起作用。
总的来说,外部变量必须在函数的外部说明一次,也应在想要使用它的任何函数中用关键字extern再次加以说明。但是只要在函数的外面说明了它,而说明又在同一个源程序文件的上面,则在函数内部就不需要再对外部变量加以说明。如果要在定义外部变量之前,或变量的定义和变量的使用不在同一个文件内,就必须使用extern加以说明。
5. 变量的作用域
变量的作用域就是指变量的有效范围,或者起作用的范围。
在一个函数内部定义的变量只在本函数范围内有效,即只有在本函数内才能使用它们,在此函数之外不能使用这些变量,称为“局部变量”。在函数之外定义的变量是外部变量,也称为“全局变量”。
局部变量可以与全局变量同名。在函数内引用这个变量时,会用到同名的局部变量,而不会用到全局变量。对于有些编译器而言,在同一个函数内可以定义多个同名的局部变量,比如在两个循环体内都定义一个同名的局部变量,则这两个局部变量的作用域就在自己所在的循环体内。如果在函数内要使用全局变量,需要使用'::'。extern
用于声明一个变量为全局变量。
静态变量的位置在静态存储区中(静态存储区在整个程序运行期间都存在)。未经初始化的全局静态变量会被程序自动初始化为0(自动变量对象的初值是任意的,除非被显示初始化)。
在局部变量之前加上关键字static,局部变量就被定义成为一个局部静态变量。局部静态变量的作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域随之结束。但是局部静态变量在离开作用域之后,并没有被销毁,而是仍然驻留在内存当中,直到程序结束,只不过我们不能再对它进行访问。
在全局变量之前加上关键字static,全局变量就被定义成为一个全局静态变量。全局静态变量的作用域与其他全局变量不同。非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其它源文件中引起错误。其他文件中可以使用相同名字的变量,不会发生冲突。
在函数的返回类型前加上关键字static,函数就被定义成为静态函数。函数的定义和声明默认情况下是extern的,但静态函数只是在声明它的文件当中可见,不能被其他文件所用。定义静态函数的好处是:其他文件中可以定义相同名字的函数,不会发生冲突;静态函数不能被其他文件所用。
[例3.1] 对已知数组中的所有元素累加求和。
[错误代码]
1 |
#include <stdio.h> |
2 |
void main( ) |
3 |
{ |
4 |
int a[]={1,2,3,4,5}; |
5 |
int sum; |
6 |
for(i=0;i<5;i++) |
7 |
sum+=a[i]; |
8 |
printf("%d\n",sum); |
9 |
} |
[编译结果]
E:\Code\3_1.cpp(5)
: error C2065: 'i' : undeclared identifier
[问题分析]
上面的编译结果提示i没有定义。把第6句改为for(int i=0;i<5;i++)后,编译通过。但运行结果为:
-858993445
明显是错误的。原因是sum未初始化为0。把第5句改为int sum=0;则结果符合预期。
[正确代码]
代码改动如下:
5 |
int sum=0; |
6 |
for(int i=0;i<5;i++) |
[运行结果]
15
静态变量的位置在静态存储区中(静态存储区在整个程序运行期间都存在)。未经初始化的全局静态变量会被程序自动初始化为0(自动变量对象的值是任意的,除非被显示地初始化)。
在局部变量之前加上关键字static,局部变量就被定义成为一个局部静态变量。局部静态变量的作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域随之结束。但是局部静态变量在离开作用域之后,并没有被销毁,而是仍然驻留在内存当中,直到程序结束,只不过我们不能再对它进行访问。
在全局变量之前加上关键字static,全局变量就被定义成为一个全局静态变量。全局静态变量的作用域与其他全局变量不同:全局静态变量在声明它的文件之外是不可见的。准确地说是从定义之处开始到文件结尾。
[例3.2] 静态变量的作用域演示。
[错误代码]
1 |
#include "stdafx.h" |
2 |
void count( ) |
3 |
{ |
4 |
static int num = 0; |
5 |
num++; |
6 |
printf("I have been called %d times\n",num); |
7 |
} |
8 |
void main( ) |
9 |
{ |
10 |
for(int i=1; i<=3; i++) |
11 |
count( ); |
12 |
printf("Total called %d times\n",num); |
13 |
} |
[编译结果]
E:\Code\3_2.cpp(14)
: error C2065: 'num' : undeclared identifier
[问题分析]
第14行代码试图使用在count函数里定义的静态局部变量num。静态局部变量num是在count函数里定义的,它的作用域局限于count函数内部。可以改用全局变量。
[正确代码]
代码改动如下:
12 |
//printf("Total called %d times\n",num); 删除此语句,或使用全局变量 |
[运行结果]
I have
been called 1 times
I have
been called 2 times
I have
been called 3 times
[例3.3] 全局静态变量的作用域演示。
[错误代码]
test1.cpp
1 |
#include "stdio.h" |
2 |
void display( ); |
3 |
extern int n; |
4 |
void main( ) |
5 |
{ |
6 |
n = 20; |
7 |
printf("%d\n",n); |
8 |
display( ); |
9 |
} |
test2.cpp
1 |
#include "stdio.h" |
2 |
static int n; |
3 |
void display( ) |
4 |
{ |
5 |
n++; |
6 |
printf("%d\n",n); |
7 |
} |
[编译结果]
Linking...
test1.obj
: error LNK2001: unresolved external symbol "int n" (?n@@
Debug/test1.exe
: fatal error LNK1120: 1 unresolved externals
Error
executing link.exe.
[问题分析]
文件分别编译通过,但link的时候test1.cpp中的变量n找不到定义,产生错误。一种解决方法是把display函数放到test1.cpp中,另一种方法是把n定义成全局变量,而不是全局静态变量。但全局变量需要初始化,而未经初始化的全局静态变量会被程序自动初始化为0。
[正确代码]
把两个文件合成一个文件,并改动如下:
1 |
#include
"stdio.h" |
2 |
static int n; |
3 |
void display(
) |
4 |
{ |
5 |
n++; |
6 |
printf("%d\n",n); |
7 |
} |
8 |
void main( ) |
9 |
{ |
10 |
n = 20; |
11 |
printf("%d\n",n); |
12 |
display( ); |
13 |
} |
[运行结果]
20
21
全局变量一般这样定义:在某个cpp文件中定义全局变量,如int gCount; 然后在要调用的cpp文件里声明extern int gCount;即可。如果还是使用int gCount,那么就会产生LNK2005错误。
头文件的重复包含可能会导致全局变量的错误声明。需要包含的头文件中往往含有变量、函数、类的定义,在其它使用的地方不得不多次被包含,如果头文件中没有相关的宏等防止重复链接的措施,那么就会产生LNK2005错误。解决办法是在需要包含的头文件中做类似的处理:
#ifndef MY_H_FILE //如果没有定义这个宏,宏的名字任意
#define MY_H_FILE //定义这个宏
int gCount;
……. //头文件主体内容
……
#endif
如果不使用宏来做,也可以使用预编译,在头文件中加入:
#pragma once
……. //头文件主体
[例3.4] 使用全局变量,求已售商品的总营业额。
[错误代码]
test1.cpp
1 |
#include "stdio.h" |
2 |
void |
3 |
float gTotal; |
4 |
void main( ) |
5 |
{ |
6 |
gTotal = 0; |
7 |
Sale(5, 20); |
8 |
Sale(32, 3); |
9 |
printf("营业额为% |
10 |
} |
test2.cpp
1 |
#include " stdio.h" |
2 |
float gTotal; |
3 |
void |
4 |
{ |
5 |
gTotal += price*count; |
6 |
} |
[编译结果]
Linking...
test2.obj
: error LNK2005: "float gTotal" (?gTotal@@3MA) already defined in
test1.obj
Debug/test1.exe
: fatal error LNK1169: one or more multiply defined symbols found
Error
executing link.exe.
[问题分析]
全局变量应该这样定义:在一个cpp文件中定义:“float gTotal;”,在其他cpp文件里声明“extern float gTotal; ”。而在本例中test1.cpp和test2.cpp里都是用的“float gTotal;”应该把其中一个改为“extern float gTotal;”。
[正确代码]
把test1.cpp中的代码改动如下:
3 |
extern float gTotal; |
[运行结果]
营业额为196.00
全局变量有很多优点,但是滥用全局变量会带来很多问题。全局变量不但阻碍了代码重用,而且使代码变得更难维护。它们阻碍代码重用是因为任何使用了全局变量的代码就立刻与之耦合,导致全局变量修改后它们也不得不跟着修改,从而使任何重用都不可能了。它们使代码变得更难维护的原因是很难甄别出哪些代码用了某个特定的全局变量,因为任何代码都有访问它们的权限。全局变量是不设防的。随便哪个维护代码的C++新手,都能使对全局变量有强烈依赖的软件随时崩溃。
有些程序员喜欢使用全局变量,认为它用起来很方便。但这样做很自私,因为软件的维护常常比它的初次开发要花费更多时间,而使用全局变量就意味着把麻烦扔给了维护工程师。在软件要交付之前,或者维护过程中,我们会发现,可能需要增加全局变量,这个变更需要大量时间,涉及到很多(甚至所有)源文件的更改。
所以,在没有必要的情况下,尽量不用全局变量。或者把对于值的访问加上了函数形式的包装,从而获得宝贵的可扩充性。
请改正如下程序中的错误。
1) 在函数里用静态变量为新学生自动按照先后顺序分配学号。
1 |
#include <stdio.h> |
2 |
void NewStudent( ) |
3 |
{ |
4 |
static int NO=0; |
5 |
NO++; |
6 |
printf("新生报到:学号为%d。\n",NO); |
7 |
} |
8 |
void main( ) |
9 |
{ |
10 |
NewStudent( ); |
11 |
printf("刚才报到的新生学号为%d。\n",NO); |
12 |
|
13 |
NewStudent( ); |
14 |
printf("刚才报到的新生学号为%d。\n",NO); |
15 |
|
16 |
NewStudent( ); |
17 |
printf("刚才报到的新生学号为%d。\n",NO); |
18 |
} |
2) 用全局变量保存学生总数,在函数AddStudent里增加新生数量,在函数Graduate里减去毕业学生数量。最后输出当前学生总数。
ex3_2.cpp
1 |
#include <stdio.h> |
2 |
#include "func.h" |
3 |
int TotalStudents; |
4 |
void main( ) |
5 |
{ |
6 |
TotalStudents = 0; |
7 |
|
8 |
AddStudent(5); |
9 |
Graduate(1); |
10 |
Graduate(2); |
11 |
|
12 |
printf("目前学校共有%d名学生。\n", TotalStudents); |
13 |
} |
func.h
1 |
#include <stdio.h> |
2 |
int TotalStudents; |
3 |
void AddStudent(int n) |
4 |
{ |
5 |
TotalStudents += n; |
6 |
} |
7 |
void Graduate(int n) |
8 |
{ |
9 |
TotalStudents -= n; |
10 |
} |
C语言中有丰富的运算符和表达式,使得C语言功能十分完善,这也是C语言的主要特点之一。
1. 运算符的种类
C语言中的运算符可分为以下几类:
(1)算术运算符
包括加(+)、减(–)、乘(*)、除(/)、求余(或称模运算,%)、自增(++)、自减(--)等。
如果参加+、–、*、/ 运算的两个数中有一个是float型或double型,则结果为double型。两个整数相除的结果为整数,舍去小数部分。但是如果除数或被除数中有一个为负值,则舍入的方向是不固定的。多数C编译系统采取“向零取整”的方法,即取整后向零的方向靠拢。
求余运算要求参与运算的量均为整型,其运算结果等于两数相除后的余数。
自增(++)、自减(--)的作用是使变量的值增1或减1,只能用于变量,不能用于常量或表达式。++或--写在变量名的前面和后面具有不同含义。例如:
++i,--i 的含义是在使用i之前,先使i的值加/减1;
i++,i-- 的含义是在使用i之后,使i的值加/减1;
(2)关系运算符
用于比较运算,包括大于(>)、小于(<)、等于(==)、 大于等于(>=)、小于等于(<=)和不等于(!=)等。
(3)逻辑运算符
用于逻辑运算。包括与(&&)、或(||)、非(!)。
(4)位操作运算符
按二进制位进行运算。包括位与(&)、位或(|)、位非(~)、位异或(^)、左移(<<)、右移(>>)等。
(5)赋值运算符
包括赋值运算符(=)及其扩展运算符,如复合算术赋值运算符(+=,-=,*=,/=,%=)和复合位运算赋值运算符(&=,|=,^=,>>=,<<=)等。
如果赋值运算符两边的数据类型不相同,系统将自动进行类型转换,即把赋值号右边的类型换成左边的类型。具体规定如下:
实型赋予整型,舍去小数部分。
整型赋予实型,数值不变,以浮点形式存放,即增加小数部分(小数部分的值为0)。
字符型赋予整型,由于字符型为一个字节,而整型为二个字节,故将字符的ASCII码值放到整型量的低八位中,高八位为0。
整型赋予字符型,只把低八位赋予字符量。
复合算术赋值运算符和复合位运算赋值运算符的写法,对初学者可能不习惯,但十分有利于编译处理,能提高编译效率并产生质量较高的目标代码。
(6)条件运算符( ? : )
这是一个三目运算符,用于条件求值。其一般形式是:
<表达式1>?<表达式2>:<表达式3>;
该运算符的含义是:先求表达式1的值,如果为真,则求表达式2 的值并把它作为整个表达式的值;如果表达式1 的值为假,则求表达式3 的值并把它作为整个表达式的值。
(7)逗号运算符( ,)
用于把若干表达式组合成一个表达式。程序中使用逗号表达式,通常是要分别求逗号表达式内各表达式的值。并不是在所有出现逗号的地方都组成逗号表达式,如在变量说明中,函数参数表中逗号只是用作各变量之间的间隔符。
(8)指针运算符
包括取内容(*)和取地址(&)二种运算。
(9)求字节数运算符
包括(sizeof),用于计算数据类型所占的字节数。
(10)特殊运算符
包括括号运算符( ),下标运算符[ ],成员运算符(→和 )等。
2. 优先级
C语言中,运算符的运算优先级共分为15级,从1级到15级逐渐降低。在表达式中,优先级较高的先于优先级较低的进行运算。而在一个运算量两侧的运算符优先级相同时,则按运算符的结合性所规定的结合方向处理。C语言中运算符的运算优先级见表4.1(从上到下逐渐降低)。
3. 结合性
在C语言的表达式中,各运算量参与运算的先后顺序不仅要遵守运算符优先级别的规定,还要受运算符结合性的制约,以便确定是自左向右进行运算还是自右向左进行运算。 这种结合性是其它高级语言的运算符所没有的,因此也增加了C语言的复杂性。
C语言中各运算符的结合性分为两种,即左结合性(自左至右)和右结合性(自右至左)。双目运算具有左结合性。例如算术运算符的结合性是自左至右,即先左后右。如有表达式x-y+z则y应先与“-”号结合,执行x-y运算,然后再执行+z的运算。这种自左至右的结合方向就称为“左结合性”。而自右至左的结合方向称为“右结合性”。 典型的右结合性运算符是赋值运算符。如x=y=z,由于“=”的右结合性,应先执行y=z再执行x=(y=z)运算。自增(++)、自减(--)也具有右结合性。C语言中运算符的结合性见表4.1。
表4.1 运算符的优先级及结合性
运算符 |
结合性 |
|
() [ ]
-> |
从左至右 |
|
! ~
++ -- +(正号) -(负号) * (指针所指内容) & sizeof |
从右至左 |
|
* / % |
从左至右 |
|
+ - |
从左至右 |
|
<< >> |
从左至右 |
|
< <= > >= |
从左至右 |
|
== != |
从左至右 |
|
&(位与) |
从左至右 |
|
^(位异或) |
从左至右 |
|
| (位或) |
从左至右 |
|
&&(逻辑与) |
从左至右 |
|
||(逻辑或) |
从左至右 |
|
?: |
从右至左 |
|
= += -= 等 |
从右至左 |
|
, |
从左至右 |
|
在进行除法运算或模运算时不能除以0,一定要做合法性检查。尤其是当运算复杂时,往往会忽视这一点。
[例4.1] 输入x,求y=1/(1-x*x)。
[错误代码]
1 |
#include "stdio.h" |
2 |
void main( ) |
3 |
{ |
4 |
float x; |
5 |
printf("请输入x:"); |
6 |
scanf("%f",&x); |
7 |
float y = 1/(1-x*x); |
8 |
printf("y=%f\n",y); |
9 |
} |
[编译结果]
编译通过。
[问题分析]
编译通过。当x不为1时,结果是正确的,如:
请输入x:2
y=-0.333333
但是当x为1时,1-x*x等于0,即分母为0,输出结果如下:
请输入x:1
y=1.#INF00
结果显然不对,应当进行合法性检验,除数为0时,应该输出警告信息。
[正确代码]
把第7~8行代码改为:
7 |
if(x==1) |
8 |
printf("除数不可以为0!\n"); |
9 |
else |
10 |
{ |
11 |
float y = 1/(1-x*x); |
12 |
printf("y=%f\n",y); |
13 |
} |
[运行结果]
请输入x:1
除数不可以为0!
%是求余运算,得到a/b的整余数。整型变量a和b可以进行求余运算,而实型变量则不允许进行“求余”运算。
[例4.2] 已知两个数,求二者相除的余数。
[错误代码]
1 |
#include "stdio.h" |
2 |
void main( ) |
3 |
{ |
4 |
float a=5,b=2; |
5 |
printf("a%%b=%d\n",a%b); |
6 |
} |
[编译结果]
e:\code\4_2.cpp(5)
: error C2296: '%' : illegal, left operand has type 'float'
e:\code\4_2.cpp(5)
: error C2297: '%' : illegal, right operand has type 'float'
[问题分析]
实型变量则不允许进行“求余”运算。
[正确代码]
代码改动如下:
4 |
int a=5,b=2; |
[运行结果]
a%b=1
=表示赋值,而==才表示我们数学中的相等关系。在许多高级语言中,用“=”符号作为关系运算符“等于”。如在BASIC程序中可以写
if (a=3) then a=b
但C语言中,“=”是赋值运算符,“==”是关系运算符。由于习惯问题,初学者往往会把:
if (a==3) a=b;
写成:
if (a=3) a=b;
本来是要判断a是否和3相等,如果a和3相等,就把b值赋给a;结果却直接把3赋给a,而且会导致程序逻辑错误。初学者往往会犯这样的错误。
[例4.3] 求555555的约数中最大的三位数是多少。
根据约数的定义,对于一个整数N,除去1和它自身,能整除N的数即为N的约数。因此,最简单的方法是用2到N-1之间的所有数去除N,即可求出N的全部约数。本例中只需求出555555的约数中最大的三位数,则其取值范围可限制在100到999之间。
[错误代码]
1 |
#include "stdio.h" |
2 |
void main( ) |
3 |
{ |
4 |
long num = 555555; |
5 |
for(int i=999;i>=100;i--) |
6 |
{ |
7 |
int j=num%i; |
8 |
if(j=0) |
9 |
{ |
10 |
printf("555555的约数中最大的三位数是%d\n",i); |
11 |
break; |
12 |
} |
13 |
} |
14 |
} |
[编译结果]
编译通过。
[问题分析]
虽然编译通过,但运行结果不正确:没有任何屏幕输出。 但实际上555555的约数肯定有三位数。问题在哪里呢?printf根本就没有执行到,是不是判断条件的问题呢?仔细检查,发现if(j=0)这一句代码里把“==”错误地写成了赋值号“=”,导致变量j被赋值后其值为0,不满足判断条件,因此永远也无法执行printf这一句代码。
[正确代码]
代码改动如下:
8 |
if(j==0) |
[运行结果]
555555的约数中最大的三位数是777
前面说过,“=”与“==”很容易被混淆;同样,将按位与运算符&与逻辑运算符&&,或者将按位或运算符|与逻辑运算符||混淆,也是很容易犯的错误。特别是C语言中按位与运算符&和按位或运算符|,与其他编程语言的按位与运算符和按位或运算符在表现形式上完全不同(如Pascal语言中分别是and和or),更容易让程序员因为受其他编程语言的影响而犯错。按位与运算符&和按位或运算符|对操作数的处理方式是将其视作一个二进制的位序列,分别对每个位进行操作。而逻辑运算符&&和||对操作数的处理方式是则是通常把0视作“假”,把非0值视为“真”。而且,逻辑运算符&&和||在左侧的操作数的值能够确定最终结果时根本不会对右侧操作数求值。
[例4.4] 找出100以内既不能被2整除又不能被3整除的整数。
[错误代码]
1 |
#include "stdio.h" |
2 |
void main( ) |
3 |
{ |
4 |
for(int i=0;i<100;i++) |
5 |
if(i%2 & i%3) |
6 |
printf("%d\t",i); |
7 |
printf("\n"); |
8 |
} |
[编译结果]
编译通过。
[问题分析]
该程序的运行结果如下:
1 7 13 19 25 31 37 43 49 55
61 67 73 79 85 91 97
但实际上5、11、17等数字也满足既不能被2整除又不能被3整除的条件。为什么这些数字被丢掉了呢?原来,第5句if(i%2 & i%3)中,错把逻辑运算符&&写成按位与运算符&了。那么这一句代码在运行中时,5%2=1,即二进制数0001;而5%3=2,即二进制数0010;二者按位进行“与”的操作,结果为0,导致结果中漏掉了“5”。其他被漏掉的数字也使基于同样原因。
[正确代码]
代码改动如下:
5 |
if(i%2 && i%3) |
[运行结果]
1 5 7 11 13 17 19 23 25 29
31 35 37 41 43 47 49 53 55 59
61 65 67 71 73 77 79 83 85 89
91 95 97
[例4.5] 依次输入5个学生的分数,如果及格,则输出及格人数。
[错误代码]
1 |
#include "stdio.h" |
2 |
void main( ) |
3 |
{ |
4 |
int nSum = 0; |
5 |
int score; |
6 |
for(int i=0;i<5;i++) |
7 |
{ |
8 |
scanf("%d",&score); |
9 |
if(score>=60) |
10 |
printf("及格人数:%d\t",nSum++); |
11 |
printf("\n"); |
12 |
} |
13 |
} |
[编译结果]
编译通过。
[问题分析]
运行结果如下:
90
及格人数:0
80
及格人数:1
70
及格人数:2
60
及格人数:3
50
当输入分数90时,此时输出的及格人数居然是0。因为第10行代码
printf("sum:%d\t", ++nSum);
里面的++nSum被写成了nSum++。前者是在使用之前就先加1,后者是使用过后再加1。所以当输入分数90时,此时输出的及格人数是0。
[正确代码]
代码改动如下:
10 |
printf("及格人数:%d\t", ++nSum); |
[运行结果]
90
及格人数:1
80
及格人数:2
70
及格人数:3
60
及格人数:4
50
[例4.6] 已知有两个小于16的二进制数hi和low,要求得到r,其低4位与low一致,高4位与hi一致。
[错误代码]
1 |
#include "stdio.h" |
2 |
void main( ) |
3 |
{ |
4 |
int hi=0xc; |
5 |
int low=0x5; |
6 |
int r = hi<<4 + low; |
7 |
printf("r=%x\n",r); |
8 |
} |
[编译结果]
E:\CODE\4_6.cpp(6)
: warning C4554: '<<' : check operator precedence for possible error; use
parentheses to clarify precedence
[问题分析]
运行结果应该是:
r=c5
但实际上却是:
r=1800
结果出乎意料。问题出在第6行:
int r = hi<<4 +
low;
编译器给出警告:warning C4554: '<<' : check
operator precedence for possible error; use parentheses to clarify precedence,建议加上括号。因为加法运算符的优先级比移位运算符的优先级高,所以实际上编译器认为:先算4+low,再把hi左移4+low位。
[正确代码]
代码改动如下:
6 |
int r = (hi<<4) + low; |
[运行结果]
r=c5
请改正如下程序中的错误。
1) 编程求1000以内的所有阿姆斯特朗数(如果一个正整数等于其各个数字的立方和,则称该数为阿姆斯特朗数。如 153=13+53+33)。
1 |
#include<stdio.h> |
2 |
void main( ) |
3 |
{ |
4 |
int a[3]; |
5 |
printf("1000以内的所有阿姆斯特朗数有:\n"); |
6 |
|
7 |
for(int i=2;i<1000;i++) /*穷举要判定的数i的取值范围2~1000*/ |
8 |
{ |
9 |
for(int t=0, k=1000; k>=10; t++) /*截取整数i的各位(从高向低位)*/ |
10 |
{ |
11 |
a[t]=(i%k)/(k/10); /*分别赋于a[0]~a[2}*/ |
12 |
k/=10; |
13 |
} |
14 |
if(a[0]*a[0]*a[0]+a[1]*a[1]*a[1]+a[2]*a[2]*a[2]=i) |
15 |
/*判断i是否为阿姆斯特朗数*/ |
16 |
printf("%5d",i); /*若满足条件,则输出*/ |
17 |
} |
18 |
printf("\n"); |
19 |
} |
2) 有1、2、3、4四个数字,能组成哪些互不相同且无重复数字的三位数?
1 |
#include<stdio.h> |
2 |
void main( ) |
3 |
{ |
4 |
int i,j,k; |
5 |
printf("\n"); |
6 |
for(i=1;i<5;i++) |
7 |
for(j=1;j<5;j++) |
8 |
for (k=1;k<5;k++) |
9 |
{ |
10 |
if (i!=k & i!=j & j!=k) /*确保i、j、k三位互不相同*/ |
11 |
printf("%d%d%d\n",i,j,k); |
12 |
} |
13 |
} |
C语言是一种结构化的程序设计语言,有很强的过程功能和数据结构功能,支持结构化的逻辑结构。从程序流程的角度来看,程序可以分为三种基本结构,即顺序结构、分支结构、循环结构。顺序结构是最简单的一种,依次执行程序中的各条语句。分支结构根据给定的条件进行判断,以决定执行某个分支程序段。循环结构的特点是:在给定条件成立时,反复执行某个程序段,直到条件不成立为止。 给定的条件称为循环条件,反复执行的程序段称为循环体。这三种基本结构可以组成所有的各种复杂程序。C语言提供了多种语句来实现这些程序结构。
控制语句用于控制程序的流程,以实现程序的各种结构方式。它们由特定的语句定义符组成。C语言的控制语句可分为条件判断语句、循环执行语句、转向语句三类。
1. 条件判断语句
(1)if语句
C语言的if语句有三种基本形式。
if
形式为:
if(表达式)
语句;
如果表达式的值为真,则执行其后的语句,否则不执行该语句。
if-else
形式为:
if(表达式)
语句1;
else
语句2;
如果表达式的值为真,则执行语句1,否则执行语句2 。
if-else
if
形式为:
if(表达式1)
语句1;
else if(表达式2)
语句2;
else if(表达式3)
语句3;
…
else if(表达式n-1)
语句 n-1;
else
语句n;
依次判断表达式的值,当出现某个值为真时,则执行其对应的语句。然后跳到整个if语句之外继续执行程序。如果所有的表达式均为假,则执行语句n 。 然后继续执行后续程序。该语句可用于多分支选择。
(2)switch语句
C语言还提供了另一种用于多分支选择的switch语句,其一般形式为:
switch(表达式){
case常量表达式1:
语句1;
break;
case常量表达式2:
语句2;
break;
…
case常量表达式n-1:
语句n-1;
break;
default :
语句n;
}
该语句首先计算表达式的值,并逐个与其后的常量表达式值相比较,当表达式的值与某个常量表达式的值相等时,即执行其后的语句。 如表达式的值与所有case后的常量表达式均不相同时,则执行default后的语句。
2. 循环执行语句
(1)for
其一般形式为:
for(表达式1;表达式2;表达式3)
语句;
其中,表达式1通常用来给循环变量赋初值。也允许在for语句外给循环变量赋初值,此时可以省略表达式1。表达式2通常是循环条件,一般为关系表达式或逻辑表达式。表达式3 通常用来修改循环变量的值。这三个表达式都可以是逗号表达式,即每个表达式都可由多个表达式组成。三个表达式都是任选项,都可以省略。
执行for循环时,首先计算表达式1的值,再计算表达式2的值,若值为真(非0)则执行循环体一次,否则跳出循环。然后再计算表达式3的值,再重新计算表达式2的值,反复循环执行。在整个for循环过程中,表达式1只计算一次,表达式2和表达式3则可能计算多次。循环体可能多次执行,也可能一次都不执行。
(2)while
其一般形式为:
while(表达式)
语句;
其中表达式是循环条件,语句为循环体。首先计算表达式的值,当值为真(非0)时,执行循环体语句。while语句中的表达式一般是关系表达或逻辑表达式。循环体如包括有一个以上的语句,则必须用{ }括起来,组成复合语句。应注意循环条件的选择以避免死循环。
(3)do-while
其一般形式为:
do
语句;
while(表达式);
其中语句是循环体,表达式是循环条件。先执行循环体语句一次,再判断表达式的值,若为真(非0)则继续循环,否则终止循环。do-while语句和while语句的区别在于do-while是先执行后判断,因此do-while至少要执行一次循环体。而while是先判断后执行,如果条件不满足,则一次循环体语句也不执行。do-while语句可以组成多重循环,而且也可以和while语句相互嵌套。当循环体由多个语句组成时,必须用{ }括起来组成一个复合语句。
3. 转向语句
(1)break
其一般格式是:
break;
break语句只能用在switch 语句或循环语句中,作用是跳出switch语句或跳出本层循环,转去执行后面的程序。使用break语句可以使循环语句有多个出口,在一些场合下使编程更加灵活、方便。
(2)goto
goto语句也称为无条件转移语句,其一般格式如下:
goto 语句标号;
其中语句标号是按标识符规定书写的符号,放在某一行语句的前面,标号后加冒号。语句标号起标识语句的作用,与goto 语句配合使用。
C语言不限制程序中使用标号的次数,但各标号不得重名。goto语句改变程序流向,转去执行语句标号所标识的语句。goto语句通常与条件语句配合使用。可用来实现条件转移,构成循环,跳出循环体等功能。但在结构化程序设计中一般不主张使用goto语句,以免造成程序流程的混乱,使理解和调试程序都产生困难。
(3)continue
continue语句只能用在循环体中,其一般格式是:
continue;
其语义是结束本次循环,即不再执行循环体中continue 语句之后的语句,直接转入下一次循环条件的判断与执行。continue语句只结束本层本次的循环,并不跳出循环。
(4)return
return语句只能出现在被调用的函数中,用于返回主调函数。
[例5.1] 把学生的成绩从百分制转换为英文等级制(A、B、C、D、E)。
[错误代码]
1 |
#include "stdio.h" |
2 |
void main( ) |
3 |
{ |
4 |
int score=0; |
5 |
char grade; //成绩等级 |
6 |
printf("请输入分数:\n"); |
7 |
scanf("%d",&score); |
8 |
while(score>=0) |
9 |
{ |
10 |
if(score>=90) |
11 |
grade = 'A'; |
12 |
else if(80<=score<90) |
13 |
grade = 'B'; |
14 |
else if(70<=score<80) |
15 |
grade = 'C'; |
16 |
else if(60<=score<70) |
17 |
grade = 'D'; |
18 |
else |
19 |
grade = 'E'; |
20 |
printf("%d : %c\n", score, grade); |
21 |
|
22 |
printf("请输入分数:\n"); |
23 |
scanf("%d",&score); |
24 |
} |
25 |
} |
[编译结果]
E:\CODE\5_1.cpp(12)
: warning C4804: '<' : unsafe use of type 'bool' in operation
E:\CODE\5_1.cpp(14)
: warning C4804: '<' : unsafe use of type 'bool' in operation
E:\CODE\5_1.cpp(16)
: warning C4804: '<' : unsafe use of type 'bool' in operation
虽然编译通过,但运行结果显然不对;79分以下也被转换为B:
请输入分数:
95
95 : A
请输入分数:
85
85 : B
请输入分数:
75
75 : B
请输入分数:
66
66 : B
请输入分数:
43
43 : B
[问题分析]
我们一定要注意C语言的条件与数学表达式之间的区别。例如我们数学中经常写到的0≤x≤9,在C语言中应该写成 x>=0 && x<=9 。上述代码的第12、14、16行代码中就犯了这个错误,导致结果出错。
[正确代码]
代码修改为:
12 |
else if(score>=80 && score<90) |
13 |
grade = 'B'; |
14 |
else if(score>=70 && score<80) |
15 |
grade = 'C'; |
16 |
else if(score>=60 && score<70) |
[运行结果]
请输入分数:
95
95 : A
请输入分数:
85
85 : B
请输入分数:
75
75 : C
请输入分数:
66
66 : D
请输入分数:
43
43 : E
当if语句中的执行语句也是if语句时,则构成了if 语句嵌套的情形。其一般形式可表示如下:
if(表达式1)
if(表达式2)
语句;
或者为
if(表达式1)
{
if(表达式2)
语句;
}
else
if(表达式3)
语句;
在嵌套内的if语句可能又是if-else型的,这将会出现多个if和多个else重叠的情况,这时要特别注意if和else的配对问题。
[例5.2] 依次输入4个点的坐标,判断该点位于哪个象限。
[错误代码]
1 |
#include "stdio.h" |
2 |
void main( ) |
3 |
{ |
4 |
for(int i=0;i<4;i++) |
5 |
{ |
6 |
int x,y; |
7 |
printf("请输入坐标点:\n"); |
8 |
scanf("%d,%d",&x,&y); |
9 |
if(x>0) |
10 |
if(y>0) |
11 |
printf("(%d,%d)位于第一象限\n",x,y); |
12 |
else if(y<0) |
13 |
printf("(%d,%d)位于第四象限\n",x,y); |
14 |
else if(x<0) |
15 |
if(y>0) |
16 |
printf("(%d,%d)位于第二象限\n",x,y); |
17 |
else if(y<0) |
18 |
printf("(%d,%d)位于第三象限\n",x,y); |
19 |
else |
20 |
printf("(%d,%d)位于原点或坐标轴上\n",x,y); |
21 |
} |
22 |
} |
[编译结果]
编译通过,但运行结果有问题:
请输入坐标点:
1,2
(1,2)位于第一象限
请输入坐标点:
-1,2
请输入坐标点:
-1,-2
请输入坐标点:
1,-4
(1,-4)位于第四象限
当x>0时输出结果是正确的,但是当x<0时,就没有输出结果了。
[问题分析]
为了避免二义性,C语言规定,else 总是与它前面最近的if配对。else最好在写每个条件时要用两个{}分别将两个分支先括起来,再添加其中的语句,以保证其配对不易错。
[正确代码]
第9行及以后代码改为:
9 |
if(x>0) |
10 |
{
|
11 |
if(y>0) |
12 |
printf("(%d,%d)位于第一象限\n",x,y); |
13 |
else if(y<0) |
14 |
printf("(%d,%d)位于第四象限\n",x,y); |
15 |
}
|
16 |
else if(x<0) |
17 |
{
|
18 |
if(y>0) |
19 |
printf("(%d,%d)位于第二象限\n",x,y); |
20 |
else if(y<0) |
21 |
printf("(%d,%d)位于第三象限\n",x,y); |
22 |
}
|
23 |
else |
24 |
printf("(%d,%d)位于原点或坐标轴上\n",x,y); |
25 |
} |
26 |
} |
[运行结果]
请输入坐标点:
1,2
(1,2)位于第一象限
请输入坐标点:
-1,2
(-1,2)位于第二象限
请输入坐标点:
-1,-2
(-1,-2)位于第三象限
请输入坐标点:
1,-4
(1,-4)位于第四象限
初学者常常会丢掉语句结束标记“;”,尤其是for语句中表达式后或do-while语句后的分号,或在if语句、while( )后、for( )后加“;”,导致流程变化。
[例5.3] 读取7个整数值(1—50),每读取一个值,程序打印出该值个数的“*”。
[错误代码]
1 |
#include "stdio.h" |
2 |
void main( ) |
3 |
{ |
4 |
int i,a,n=1; |
5 |
while(n<=7) |
6 |
{ |
7 |
do { |
8 |
scanf("%d",&a); |
9 |
}while(a<1||a>50) |
10 |
for(i=1;i<=a;i++); |
11 |
printf("*"); |
12 |
printf("\n"); |
13 |
n++; |
14 |
} |
15 |
} |
[编译结果]
E:\CODE\5_3.cpp(10)
: error C2143: syntax error : missing ';' before 'for'
原来是第9行代码“}while(a<1||a>50)”后面没有写分号。更正错误后,编译通过,但运行结果不对:
3
*
9
*
32
*
5
*
7
*
9
*
1
*
[问题分析]
do-while语句用于首先执行一次循环体语句,然后开始测试循环条件,当条件为‘真’时继续循环的处理过程。语句格式:
do{
语句;
}while(表达式);
使用中要注意最后一行的分号不要忘了。本例就犯了这样的错误。
另外,第10行代码“for(i=1;i<=a;i++);”后面多了一个分号,导致循环体内没有执行任何语句,只在循环结束后执行了一次“printf("*");”,所以不管输入几,都只输出了一个“*”号。
[正确代码]
代码修改为:
9 |
}while(a<1||a>50); |
10 |
for(i=1;i<=a;i++) //此处删除分号 |
[运行结果]
3
***
9
*********
32
********************************
5
*****
7
*******
9
*********
1
*
C语言的switch语句的控制流程能够依次通过并执行各个case部分。但是要注意,如果case语句后面没有写break,则程序的控制流程会径直通过case标号,而不受任何影响。
[例5.4] 依次输入3次整数color,当color值分别为1,2,3时,分别输出“red”、“green”、“blue”。
[错误代码]
1 |
#include "stdio.h" |
2 |
void main( ) |
3 |
{ |
4 |
for(int i=0;i<3;i++) |
5 |
{ |
6 |
int color; |
7 |
scanf("%d", &color); |
8 |
switch(color) |
9 |
{ |
10 |
case 1: |
11 |
printf("red\n"); |
12 |
case 2: |
13 |
printf("green\n"); |
14 |
case 3: |
15 |
printf("blue\n"); |
16 |
} |
17 |
} |
18 |
} |
[编译结果]
编译通过,但运行结果有问题:
1
red
green
blue
2
green
blue
3
Blue
[问题分析]
由于漏写了break语句,case只起标号的作用,而不起判断作用。因此,如果用户输入1,则程序会在输出了“red”之后,继续输出“green”、“blue”。正确写法应在每个分支后再加上“break;”。
[正确代码]
把第8行及以后的代码改写为:
8 |
switch(color) |
9 |
{ |
10 |
case 1: |
11 |
printf("red\n"); |
12 |
break; |
13 |
case 2: |
14 |
printf("green\n"); |
15 |
break; |
16 |
case 3: |
17 |
printf("blue\n"); |
18 |
break; |
19 |
} |
20 |
} |
21 |
} |
[运行结果]
1
red
2
green
3
blue
[例5.5] 输入整数i,求i+(i+1)+(i+2)+……
+ 10。
[错误代码]
1 |
#include "stdio.h" |
2 |
void main( ) |
3 |
{ |
4 |
int a=0,i; |
5 |
scanf("%d",&i); |
6 |
do |
7 |
{ |
8 |
a=a+i; |
9 |
i++; |
10 |
} |
11 |
while(i<=10); |
12 |
printf("%d\n",a); |
13 |
} |
[编译结果]
编译通过。但是当i>10时结果有问题:
11
11
[问题分析]
可以看到,当输入i的值小于或等于10时,结果正确。而当i>10时,结果错误。因为while循环是先判断后执行,而do-while循环是先执行后判断。对于大于10的数while循环一次也不执行循环体,而do-while语句则要执行一次循环体。
[正确代码]
把第6行及以后的代码改写为:
6 |
while(i<=10) |
7 |
{ |
8 |
a=a+i; |
9 |
i++; |
10 |
} |
11 |
printf("%d\n",a); |
12 |
} |
[运行结果]
11
0
在C/C++等高级编程语言中保留了goto语句,但建议不用或少用。任何程序都可以用顺序、分支和循环结构表示出来。从高级程序语言中去掉goto语句并不影响高级程序语言的编程能力,而且编写的程序的结构更加清晰。
goto语句破坏了程序结构,会使程序结构难于理解,使程序可读性变差。滥用goto语句,可能会导致杂乱的、令人混淆的代码(通常称之为面条式代码)。结构化程序设计方法主张限制使用goto语句,但也不是绝对禁止使用goto语句;一般来说,goto语句主要有两种用途:
1)与if语句一起构成循环结构。
2)从循环体内跳转到循环体外。在C语言中可以使用break语句和continue语句跳出本层循环和结束本次循环,goto语句的使用机会已大大减少,只是需要从多层循环的内层循环跳到外层循环时才用到goto语句。但是这种用法不符合结构化原则,一般不宜采用,只有在不得已时(例如能够大大提高效率)才使用。
[例5.6] 计算奖金:工作量超过1000,奖金为2000元;工作量801~1000,奖金为1000元;工作量301~800,奖金为500元;工作量低于或等于300,奖金为0;工作量为负数时程序结束。
[错误代码]
1 |
#include "stdio.h" |
2 |
void main( ) |
3 |
{ |
4 |
int bonus=0, count=0; |
5 |
|
6 |
A: printf("请输入您的工作量:\n"); |
7 |
scanf("%d",&count); |
8 |
if(count == -1) |
9 |
goto END; |
10 |
else if(count>1000) |
11 |
bonus = 2000; |
12 |
else if(count>800) |
13 |
bonus = 1000; |
14 |
else if(count>300) |
15 |
bonus = 500; |
16 |
else |
17 |
bonus = 0; |
18 |
|
19 |
printf("您的奖金为:%d\n",bonus); |
20 |
goto A; |
21 |
END: |
22 |
printf("再见\n"); |
23 |
} |
[编译结果]
编译通过,运行结果也没问题。
[问题分析]
在本例中没有必要使用goto语句。用while循环即可。使用goto语句不符合结构化原则,一般不宜采用,只有在不得已时(例如能够大大提高效率)才使用。严格地说,本例代码不算错误,但不建议使用goto语句。
[正确代码]
1 |
#include "stdio.h" |
2 |
void main( ) |
3 |
{ |
4 |
int bonus=0, count=0; |
5 |
|
6 |
while(count>=0) |
7 |
{ |
8 |
printf("请输入您的工作量:\n"); |
9 |
scanf("%d",&count); |
10 |
|
11 |
if(count>1000) |
12 |
bonus = 2000; |
13 |
else if(count>800) |
14 |
bonus = 1000; |
15 |
else if(count>300) |
16 |
bonus = 500; |
17 |
else |
18 |
bonus = 0; |
19 |
|
20 |
printf("您的奖金为:%d\n",bonus); |
21 |
} |
22 |
printf("再见\n"); |
23 |
} |
[运行结果]
请输入您的工作量:
1200
您的奖金为:2000
请输入您的工作量:
900
您的奖金为:1000
请输入您的工作量:
500
您的奖金为:500
请输入您的工作量:
200
您的奖金为:0
请输入您的工作量:
-1
再见
请改正如下程序中的错误。
1) 鸡兔共有30只,鸡兔总共有90只脚,请问有多少只鸡,多少只兔?
1 |
#include<stdio.h> |
2 |
void main( ) |
3 |
{ |
4 |
for(int i=0;i<30;i++); |
5 |
for(int j=0;j<30;j++) |
6 |
if(i+j==30 && 2*i+4*j==90) |
7 |
printf("有%d只鸡,%d只兔\n",i,j); |
8 |
} |
2) 编写一个简单的计算器,能根据用户输入的两个操作数和运算符来计算加减乘除。
1 |
#include<stdio.h> |
2 |
void main( ) |
3 |
{ |
4 |
float a,b; |
5 |
int op; |
6 |
printf("请输入两个操作数:\n"); |
7 |
scanf("%f,%f",&a,&b); |
8 |
printf("请选择运算符:\n"); |
9 |
printf("1)加 2)减 3)乘 4)除\n"); |
10 |
scanf("%d",&op); |
11 |
switch(op) |
12 |
{ |
13 |
case 1: //加 |
14 |
printf("%f+%f=%f\n",a,b,a+b); |
15 |
case 2: //减 |
16 |
printf("%f-%f=%f\n",a,b,a-b); |
17 |
case 3: //乘 |
18 |
printf("%f*%f=%f\n",a,b,a*b); |
19 |
case 4: //除 |
20 |
if(b==0) |
21 |
printf("分母为0!\n"); |
22 |
else |
23 |
printf("%f/%f=%f\n",a,b,a/b); |
24 |
default: |
25 |
printf("运算符错误!\n"); |
26 |
} |
27 |
} |
函数是C程序的基本模块,通过对函数模块的调用来实现特定的功能。C语言提供了极为丰富的库函数,还允许用户自定义函数。库函数由C系统提供,用户无须定义,也不必在程序中作类型说明,只需在程序前包含有该函数原型的头文件,即可在程序中直接调用。常用的库函数有printf、scanf、getchar、putchar、gets、puts、strcat等。函数定义的一般形式为:
类型说明符 函数名(形式参数表)
{
类型说明
语句
}
其中类型说明符和函数名称为函数头。类型说明符指明了本函数的类型,实际上是函数返回值的类型。函数名是由用户定义的标识符,函数名后有一个括号,其中有形式参数表,形式参数表可以为空,也就是可以不含参数。{ } 中的内容称为函数体。在函数体中也有类型说明,这是对函数体内部所用到的变量的类型说明。如果不要求函数有返回值,此时函数类型说明符可以写为void。
在程序中是通过对函数的调用来执行函数体的,其过程与其它语言的子程序调用相似。在函数调用时也必须给出参数,称为实际参数(简称为实参)。 进行函数调用时,主调函数将把实参的值传送给形参,供被调函数使用。
C语言允许函数的递归调用。一个函数在函数体内调用其自身称为递归调用,这种函数称为递归函数。在递归调用中,主调函数同时又是被调函数。执行递归函数将反复调用其自身。为了防止递归调用无终止地进行,必须在函数内加条件判断,满足某种条件后就不再作递归调用,然后逐层返回。
[例6.1] 输入一个角度,求其正弦值。
[错误代码]
1 |
#include "stdio.h" |
2 |
#define PI 3.14 |
3 |
void main( ) |
4 |
{ |
5 |
int angle; |
6 |
printf("请输入角的大小(度):\n"); |
7 |
scanf("%d",&angle); |
8 |
printf("该角的正弦值为:% |
9 |
} |
[编译结果]
E:\CODE\6_1.cpp(8)
: error C2065: 'sin' : undeclared identifier
[问题分析]
数学函数的声明都在math.h里,本例代码忘了包含math.h,所以编译器认为'sin'没有定义。
代码里的“angle*PI/
[正确代码]
在前面增加代码:
1 |
#include
"math.h" |
[运行结果]
请输入角的大小(度):
30
该角的正弦值为:0.50
[例6.2] 编写函数,分别计算球、圆柱、圆锥的体积。
[错误代码]
1 |
#include "stdio.h" |
2 |
#define PI 3.14 |
3 |
void main( ) |
4 |
{ |
5 |
float radius = 2; |
6 |
float height = 5; |
7 |
printf("球的体积为:% |
8 |
printf("圆柱的体积为:% |
9 |
printf("圆锥的体积为:% |
10 |
} |
11 |
double BallVolume(float radius) //计算球的体积 |
12 |
{ |
13 |
return PI*radius*radius*radius*4.0/3.0; |
14 |
} |
15 |
double CylinderVolume(float radius, float height) //计算圆柱的体积 |
16 |
{ |
17 |
return PI*radius*radius*height; |
18 |
} |
19 |
double ConeVolume(float radius, float height) //计算圆锥的体积 |
20 |
{ |
21 |
return PI*radius*radius*height/3.0; |
22 |
} |
[编译结果]
E:\CODE\6_2.cpp(7)
: error C2065: 'BallVolume' : undeclared identifier
E:\CODE\6_2.cpp(8)
: error C2065: 'CylinderVolume' : undeclared identifier
E:\CODE\6_2.cpp(9)
: error C2065: 'ConeVolume' : undeclared identifier
E:\CODE\6_2.cpp(13)
: error C2373: 'BallVolume' : redefinition; different type modifiers
E:\CODE\6_2.cpp(18)
: error C2373: 'CylinderVolume' : redefinition; different type modifiers
E:\CODE\6_2.cpp(23)
: error C2373: 'ConeVolume' : redefinition; different type modifiers
[问题分析]
从编译结果看,编译器认为函数BallVolume、CylinderVolume、ConeVolume未定义。原因就是使用函数之前未声明。即使函数已经写好,只要没有声明,编译器就会给出这样的错误信息。
[正确代码]
在main函数之前增加函数BallVolume、CylinderVolume、ConeVolume的声明:
3 |
double BallVolume(float radius); |
4 |
double CylinderVolume(float radius, float height); |
5 |
double ConeVolume(float radius, float height); |
[运行结果]
球的体积为:33.49
圆柱的体积为:62.80
圆锥的体积为:20.93
[例6.3] 将整数数组A和B的对应元素相加,结果保存到整数数组C的对应元素中。
[错误代码]
1 |
#include "stdio.h" |
2 |
#define LENGTH 5 |
3 |
void AddArray(int arrayA[], int arrayB[], int arrayC[], int n) |
4 |
{ |
5 |
for(int i=0;i<n;i++) |
6 |
{ |
7 |
arrayC[i] = arrayA[i] + arrayB[i]; |
8 |
} |
9 |
} |
10 |
void main( ) |
11 |
{ |
12 |
int A[LENGTH] = {1,3,5,8,4}; |
13 |
int B[LENGTH] = {6,9,15,9,0}; |
14 |
int C[LENGTH]; |
15 |
for(int i=0;i<LENGTH;i++) |
16 |
{ |
17 |
AddArray(A[i],B[i],C[i],LENGTH); |
18 |
printf("%d\t", C[i]); |
19 |
} |
20 |
printf("\n"); |
21 |
} |
[编译结果]
E:\CODE\6_3.cpp(17)
: error C2664: 'AddArray' : cannot convert parameter 1 from 'int' to 'int []'
Conversion
from integral type to pointer type requires reinterpret_cast, C-style cast or
function-style cast
[问题分析]
本例中函数实参格式不对,函数AddArray的形参是数组,实参却传递了数组中的元素,即一个整数。所以编译器给出错误信息:无法把参数1从'int' 转换为 'int []'。
[正确代码]
把第15行及以后代码改为:
15 |
AddArray(A,B,C,LENGTH); |
16 |
for(int
i=0;i<LENGTH;i++) |
17 |
printf("%d\t", C[i]); |
18 |
printf("\n"); |
19 |
} |
[运行结果]
7 12 20 17 4
[例6.4] 输入两个浮点数,判断是否可以相除,如果可以相除则输出结果,否则提示“分母为0!”。
[错误代码]
1 |
#include "stdio.h" |
2 |
bool CanDivide(float a,float b) |
3 |
{ |
4 |
bool c = (b!=0); |
5 |
} |
6 |
void main( ) |
7 |
{ |
8 |
float x; |
9 |
float y; |
10 |
scanf("%f,%f",&x,&y); |
11 |
if(CanDivide(x,y)) //判断是否可以相除 |
12 |
printf("a/b=% |
13 |
else |
14 |
printf("分母为0!\n"); |
15 |
} |
[编译结果]
E:\CODE\6_4.cpp(5)
: error C4716: 'CanDivide' : must return a value
[问题分析]
编译器很明白地给出了错误信息:函数'CanDivide'必须返回一个值。
[正确代码]
代码修改如下:
4 |
return b!=0; |
[运行结果]
8,0
分母为0!
[例6.5] 用递归计算10*9*8*7*6*5*4*3*2*1。
[错误代码]
1 |
#include "stdio.h" |
2 |
int factorial(int j) |
3 |
{ |
4 |
int sum; |
5 |
if(j==j-1) |
6 |
sum=1; |
7 |
else |
8 |
{ |
9 |
printf("%d",j); |
10 |
if(j>1) printf("*"); |
11 |
sum=j*factorial(j-1); |
12 |
} |
13 |
return sum; |
14 |
} |
15 |
void main( ) |
16 |
{ |
17 |
printf("=%d\n",factorial(10)); |
18 |
} |
[编译结果]
编译通过。但运行结果出错:
……
6698-6699-6700-6701-6702-6703-6704-6705-6706-6707-6708-6709-6710-6711-6712-6713-6714-6715-6716-6717-6718-6719-6720-6721-6722-6723-6724-6725-6726-6727-6728-6729-6730-6731-6732-6733-6734-6735-6736-6737-6738-6739-6740-6741-6742-6743-6744-6745-6746-6747-6748-6749-6750-6751-6752-6753-6754-6755-6756-6757-6758-6759-6760-6761-6762-6763-6764-6765-6766-6767-6768-6769-6770-6771-6772-6773-6774-6775-6776-6777-6778-6779-6780-6781-6782-6783-6784-6785-6786-6787-6788-6789-6790-6791-6792-6793-6794-6795-6796-6797-6798-6799-6800-6801-6802-6803-6804-6805-6806-6807-6808-6809-6810-6811-6812-6813-6814-6815-6816-6817-6818-6819-6820-6821-6822
……
[问题分析]
在递归时忘了设置边界条件或设置了错误的边界条件,容易造成死循环。在本例中,从运行结果来看,程序陷入了死循环。显然是第5行代码中的边界条件是错的:“if(j==j-1)”。
[正确代码]
5 |
if(j==0) |
[运行结果]
10*9*8*7*6*5*4*3*2*1=3628800
请改正如下程序中的错误。
1) 编写一个函数,判断是否闰年。
1 |
void main( ) |
2 |
{ |
3 |
int year; |
4 |
printf("请输入年份:\n"); |
5 |
scanf("%d",&year); |
6 |
if(IsLeapYear(year)) |
7 |
printf("%d年是闰年\n",year); |
8 |
else |
9 |
printf("%d年不是闰年\n",year); |
10 |
} |
11 |
|
12 |
bool IsLeapYear(int year) |
13 |
{ |
14 |
return (year%4==0 && year%100!=0) || (year%400==0); |
15 |
} |
2) 编写一个函数,判断一个数是否为素数。
1 |
#include <stdio.h> |
2 |
#include <math.h> |
3 |
bool IsPrimeNumber(int x) |
4 |
{ |
5 |
int i; |
6 |
if(x<2) return 0; |
7 |
for(i=2;i<=sqrt(x);i++) |
8 |
if(x%i==0) |
9 |
return false; |
10 |
} |
11 |
|
12 |
void main( ) |
13 |
{ |
14 |
int n; |
15 |
while(scanf("%d",&n)!=EOF) |
16 |
{ |
17 |
if(IsPrimeNumber( ))printf("YES\n"); |
18 |
else printf("NO\n"); |
19 |
} |
20 |
} |
3) 编写一个函数,从键盘获得输入,然后反向打印它们。
1 |
#include <stdio.h> |
2 |
void antitone( ) |
3 |
{ |
4 |
char ch; |
5 |
if((ch=getchar( )) != '\n ') |
6 |
antitone( ); |
7 |
putchar(ch); |
8 |
} |
9 |
void main( ) |
10 |
{ |
11 |
antitone( ); |
12 |
} |
C语言中应用了一个预处理程序,在将源代码送到编译程序之前,预先由预处理程序处理。C语言提供了多种预处理功能,如宏定义、文件包含、 条件编译等。合理地使用预处理功能编写的程序便于阅读、修改、 移植和调试,也有利于模块化程序设计。预处理命令以“#”开头,如包含命令#include、宏定义命令#define等。
1.宏定义(#define)
在C语言源程序中允许用一个标识符来表示一个字符串,称为“宏”。被定义为“宏”的标识符称为“宏名”。在编译预处理时,对程序中所有出现的“宏名”,都用宏定义中的字符串去代换,称为“宏代换”或“宏展开”。“宏”分为有参数和无参数两种。无参宏定义的一般形式为:
#define 标识符 字符串
例如:
#define PI 3.1415926536
对带参数的宏,在调用中,不仅要宏展开,而且要用实参去代换形参。其一般形式为:
#define 宏名(形参表)
字符串
例如:
#define MAX(a,b) (a>b)?a:b
宏展开只是一种简单的代换,字符串中可以含任何字符,可以是常数或者表达式,预处理程序对它不作任何检查。只能在编译已被宏展开后的源程序时才能发现错误。宏定义在行末不必加分号。宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。宏名在源程序中若用引号括起来,则预处理程序不对其作宏代换。宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名。习惯上宏名用大写字母表示,以便于与变量区别。
在带参宏定义中,宏名和形参表之间不能有空格出现。形式参数不分配内存单元,因此不必作类型定义。在宏定义中,字符串内的形参通常要用括号括起来以避免出错。
2.文件包含(#include)
文件包含命令的功能是把指定的文件插入该命令行位置,并取代该命令行,从而把指定的文件和当前的源程序文件连成一个源文件。文件包含是C预处理程序的另一个重要功能。文件包含命令的一般形式为:
#include "文件名" 或者 #include <文件名>
例如,用该命令包含库函数的头文件:
#include "stdio.h"
#include "math.h"
包含命令中的文件名可以用双引号括起来,也可以用尖括号括起来。但是这两种形式是有区别的:使用尖括号表示在包含文件目录中去查找(包含目录是由用户在设置环境时设置的),而不在源文件目录去查找;使用双引号则表示首先在当前的源文件目录中查找,若未找到才到包含目录中去查找。 一个include命令只能指定一个被包含文件,若有多个文件要包含,则需用多个include命令。文件包含允许嵌套,即在一个被包含的文件中又可以包含另一个文件。
3.条件编译
预处理程序提供了条件编译功能。
可以按不同的条件去编译不同的程序部分,因而产生不同的目标代码文件。条件编译允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率。使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计。条件编译有三种形式,下面分别介绍:
第一种形式:
#ifdef 标识符
程序段1
#else
程序段2
#endif
它的功能是,如果标识符已被 #define命令定义过则对程序段1进行编译;否则对程序段2进行编译。如果没有程序段2(为空),本格式中的#else可以没有。
第二种形式:
#ifndef 标识符
程序段1
#else
程序段2
#endif
与第一种形式的区别是将“ifdef”改为“ifndef”。它的功能是,如果标识符未被#define命令定义过则对程序段1进行编译,否则对程序段2进行编译。
第三种形式:
#if 常量表达式
程序段1
#else
程序段2
#endif
它的功能是,如常量表达式的值为真(非0),则对程序段1 进行编译,否则对程序段2进行编译。
[例7.1] 输入球的半径,求该球的表面积。
[错误代码]
1 |
#include "stdio.h" |
2 |
#define PI=3.14; |
3 |
void main( ) |
4 |
{ |
5 |
float radius; |
6 |
printf("请输入球的半径:\n"); |
7 |
scanf("%f",&radius); |
8 |
double S = PI*radius*radius*4.0; |
9 |
printf("球的表面积为:% |
10 |
} |
[编译结果]
E:\code\7_1.cpp(2)
: error C2008: '=' : unexpected in macro definition
E:\CODE\7_1.cpp(8)
: error C2100: illegal indirection
E:\CODE\7_1.cpp(8)
: warning C4552: '*' : operator has no effect; expected operator with side-effect
[问题分析]
宏定义的一般形式可以写成:
#define string1 string2
其中的两个参数string1和string2是两个字符串,程序中出现的string1都由string2来代替。请注意在使用#define时候的一些常见错误:
#define MAX = 100
#define MAX 100;
前者程序中出现的“MAX”都被替换成“= 100”,后者程序中出现的“MAX”都被替换成“100;”,因而都是错的。本例中,第2行代码“#define
PI=3.14;”里的“=”应该换成空格,而且后面不加分号。
[正确代码]
代码修改为:
2 |
#define PI 3.14 |
[运行结果]
请输入球的半径:
1
球的表面积为:12.56
[例7.2] 利用带参数的宏替换计算27被3的平方除。
[错误代码]
1 |
#include "stdio.h" |
2 |
#define square(n) n*n |
3 |
void main( ) |
4 |
{ |
5 |
printf("%f\n", 27.0/square(3.0)); |
6 |
} |
[编译结果]
编译通过,但运行结果错误:
27.000000
[问题分析]
第2行代码为“#define
square(n) n*n”,则第5行代码变为:
printf("%f\n",
27.0/3.0*3.0);
由于运算是从左往右运行的,所以结果为:
(27.0/3.0)*3.0 = 27.0
要避免这种问题,应该在作宏定义时,用括号把整个宏扩展都括起来。
[正确代码]
代码修改为:
2 |
#define square(n) (n*n) |
[运行结果]
3.000000
[例7.3] 计算整数1至10的平方值。
[错误代码]
1 |
#include "stdio.h" |
2 |
#define square(n) (n*n) |
3 |
void main( ) |
4 |
{ |
5 |
int i = 1; |
6 |
while(i<=10) |
7 |
printf("%d\n", square(i++)); |
8 |
} |
[编译结果]
编译通过,但运行结果错误:
1
9
25
49
81
只输出了1、3、5、7、9的平方。
[问题分析]
如下宏调用
#define square(n) (n*n)
把square(i++)变为
(i++*i++)
预处理程序在进行字符串进行替换时,是原封不动地、忠实地进行替换。
[正确代码]
把第6行及以后的代码改为:
6 |
while(i<=10) |
7 |
{ |
8 |
printf("%d\n", square(i)); |
9 |
i++; |
10 |
} |
11 |
} |
[运行结果]
1
4
9
16
25
36
49
64
81
100
[例7.4] 定义一个宏,实现把数值减1的功能。
[错误代码]
1 |
#include "stdio.h" |
2 |
#define f (x) ((x)-1) |
3 |
void main( ) |
4 |
{ |
5 |
int n=5; |
6 |
printf("n=%d\n",n); |
7 |
printf("n-1=%d\n",f(n)); |
8 |
} |
[编译结果]
E:\CODE\7_4.cpp(7)
: error C2065: 'x' : undeclared identifier
[问题分析]
前面说过,宏定义的一般形式可以写成:
#define string1 string2
其中的两个参数string1和string2是两个字符串,程序中出现的string1都由string2来代替。本例中,第2行代码中,“f”与“(x)”之间多了空格,导致程序中的“f”都会被替换成“(x) ((x)-1)”,所以编译时才会认为第7行代码中有一个未定义的标识符x。
[正确代码]
代码修改为:
2 |
#define f(x) ((x)-1) |
[运行结果]
n=5
n-1=4
请改正如下程序中的错误。
1) 输入两个整数,输出二者之和的两倍。
1 |
#include <stdio.h> |
2 |
#define DOUBLE(a) 2*a; |
3 |
void main( ) |
4 |
{ |
5 |
int m,n; |
6 |
scanf("%d",&m); |
7 |
scanf("%d",&n); |
8 |
printf("%d与%d之和的两倍是:%d\n", m, n, DOUBLE(m+n)); |
9 |
} |
2) 计算并输出1*2+2*3+3*4+……+8*9。
1 |
#include <stdio.h> |
2 |
#define M(n) (n*(n+1)) |
3 |
void main( ) |
4 |
{ |
5 |
int i=1; |
6 |
int sum=0; |
7 |
while(i<9) |
8 |
{ |
9 |
sum += M(i++); |
10 |
} |
11 |
printf("%d\n", sum); |
12 |
} |
指针是C语言中广泛使用的一种数据类型,利用指针变量可以表示各种数据结构,能很方便地使用数组和字符串,并能象汇编语言一样处理内存地址,从而编出精练而高效的程序。指针极大地丰富了C语言的功能。
1.指针和地址
在计算机中,所有的数据都是存放在存储器中的。 一般把存储器中的一个字节称为一个内存单元,不同的数据类型所占用的内存单元数不等,如整型量占2个单元,字符量占1个单元等。为了正确地访问这些内存单元,必须为每个内存单元编号,内存单元的编号也叫做地址,通常也称为指针。 内存单元的指针和内存单元的内容是两个不同的概念。定义指针的目的是为了通过指针去访问内存单元。指针是一个数据结构的首地址。
定义指针变量的一般形式为:
类型说明符 *变量名;
其中,*表示这是一个指针变量,变量名即为定义的指针变量名,类型说明符表示本指针变量所指向的变量的数据类型。
未经赋值的指针变量不能使用,否则将造成系统混乱,甚至死机。指针变量的赋值只能赋予地址,否则将引起错误。C语言中提供了地址运算符&来表示变量的地址。如:
int val;
int *p = &val;
意为把整型变量val 的地址赋予p。C语言中还提供了指针运算符*,*p是指针变量p所指向的对象。
2.指针和函数参数
前面提到,函数的参数可以是整型、浮点型、字符型等数据类型,这种方式传递实际参数给形式参数,传递过程是单向的值拷贝过程,形式参数的变化不会影响到实际参数的值,因此只能通过return语句将函数的结果返回,无法返回多个值。如果一个函数需要有多个输出,可以将指针变量作为函数的参数,它的作用是将一个变量的地址传送给被调用函数的形参。在函数执行过程中使指针变量所指向的变量值发生变化,函数调用结束后,这些变量值的变化依然会保留下来。请注意,不能企图通过改变形参指针变量的值而使实参指针变量的值改变。
3.指针和数组
在程序设计中,为了处理方便,把具有相同类型的若干变量按有序的形式组织起来。这些按序排列的同类数据元素的集合称为数组。在C语言中,数组属于构造数据类型。一个数组可以分解为多个数组元素,这些数组元素可以是基本数据类型或是构造类型。因此按数组元素的类型不同,数组又可分为数值数组、字符数组、指针数组、结构数组等各种类别。
数组说明的一般形式为:
类型说明符 数组名 [常量表达式],……;
其中,类型说明符是任一种基本数据类型或构造数据类型。数组名是用户定义的数组标识符。 方括号中的常量表达式被称为数组的长度,表示数组中数据元素的个数。例如:
int val[5]; 说明整型数组val,有5个元素。
char c[8]; 说明字符数组c,有8个元素。
float f[12]; 说明实型数组f,有12个元素。
使用数组需要注意以下几点:
允许在同一个类型说明中,说明多个数组和多个变量。
数组名不能与其它变量名相同
同一个数组中,所有元素的数据类型都是相同的。数组的类型实际上是指数组元素的取值类型。
数组名的书写规则应符合标识符的书写规定。
方括号中常量表达式表示数组元素的个数,但其下标从0开始计算。
不能在方括号中用变量来表示元素的个数,但是可以是符号常数或常量表达式。
在C语言中,指针和数组有很密切的关系。任何可通过数组下标完成的操作都可以通过指针完成。一般来说,用指针更快,但对初学者来说比较难于掌握。例如:
int a[10];
定义了一个长度为10的数组a,包含10个地址相连的元素:a[0],a[1],a[2],… ,a[9]。如下语句:
int *p = &a[0];
使指针p指向数组a的第0个元素,即p包含a[0]的地址,p+i指向p后的第i个元素,*(p+i)是a[i]的内容。对a[i]的引用也可以写成*(a+i)。int *p = &a[0];也可以写成int *p = a;
但是数组名和指针之间有一个区别:指针是变量,p=a和p++都是有意义的操作;但数组名是常量,不是变量,因此象a=p,a++,或p=&a之类的表达式都是非法的。
4.多维数组
只有一个下标的数组称为一维数组,其数组元素也称为单下标变量。在实际问题中有很多量是二维或多维的,因此C语言允许构造多维数组。有多个下标的数组称为多维数组,其数组元素也称为多下标变量。以二维数组为例,其类型说明的一般形式是:
类型说明符 数组名[常量表达式1][常量表达式2]…;
其中常量表达式1表示第一维下标的长度,常量表达式2 表示第二维下标的长度。例如:
int a[3][4];
说明了一个三行四列的数组,数组名为a,其下标变量的类型为整型。该数组的下标变量共有3×4个,即:
a[0][0],a[0][1],a[0][2],a[0][3]
a[1][0],a[1][1],a[1][2],a[1][3]
a[2][0],a[2][1],a[2][2],a[2][3]
二维数组在概念上是二维的,即是说其下标在两个方向上变化,但是,实际的硬件存储器却是连续编址的,也就是说存储器单元是按一维线性排列的。如何在一维存储器中存放二维数组呢?在C语言中,是按行排列的,即放完一行之后顺次放入第二行。以a[3][4]为例,先存放a[0]行,再存放a[1]行,最后存放a[2]行。由于数组a说明为int类型,在Visual C++中,该类型占4个字节的内存空间,所以每个元素均占有4个字节。
5.指针数组
一个数组的元素值为指针则构成指针数组。指针数组是一组有序的指针的集合。指针数组的所有元素都必须是具有相同存储类型和指向相同数据类型的指针变量。
指针数组说明的一般形式为:
类型说明符 *数组名[数组长度]
其中,类型说明符为指针值所指向的变量的类型。例如:int *p[2] 表示p是一个指针数组,它有两个数组元素,每个元素值都是一个指针,指向整型变量。通常可用一个指针数组来指向一个二维数组。
6.指向指针的指针
如果一个指针变量存放的内容是另一个指针变量的地址,则称为指向指针的指针。通过指针访问变量称为间接访问。如果通过指向指针的指针来访问变量则构成了二级或多级间接访问。在C语言程序中,对间接访问的级数并未明确限制,但是间接访问级数太多时不容易理解,也容易出错,因此,一般很少超过二级间接访问。 指向指针的指针变量说明的一般形式为:
类型说明符** 指针变量名;
例如: int** p; 表示p是一个指针变量,它指向另一个指针变量,而这个指针变量指向一个整型量。
7.指针与多维数组
C语言把二维数组看作是一维数组的集合,即二维数组是一个元素为一维数组的特殊一维数组。多维数组可以依次类推。例如二维数组
int a[m][n];
可以看作是由m个一维数组
a[0]、a[1]、… 、a[m-2]、a[m-1]
构成。这m个一维数组都有n个元素,即每个a[i]都是由n个int类型的变量
a[i][0]、a[i][1]、… 、a[i][n-1]
组成的int类型的数组。
8.指向函数的指针
在C语言中,函数本身不是变量,但可以定义指向函数的指针,这种指针可以被赋值、存放于数组之中,传递给函数及作为函数的返回值等。指向函数的指针是个变量,因而能够被赋值,从而指向不同的函数。
[例8.1] 利用循环给数组各元素赋值为0~9,然后按照逆序输出各元素的值。
[错误代码]
1 |
#include "stdio.h" |
2 |
void main( ) |
3 |
{ |
4 |
int a[10]; |
5 |
for(int i=1;i<=10;i++) |
6 |
{ |
7 |
a[i] = i-1; |
8 |
} |
9 |
for(i=10;i>=1;i--) |
10 |
{ |
11 |
printf("%d\t", a[i]); |
12 |
} |
13 |
printf("\n"); |
14 |
} |
[编译结果]
编译通过,但运行时报错:
图8.1数组越界
[问题分析]
本例中,定义了int数组a[10],下标采用循环变量i,从1到10。实际上,数组下标应该是从0到9,即10个数组元素是a[0] 、a[1]、a[2]、a[3]、a[4]、a[5]、a[6]、a[7]、a[8]、a[9],不包括a[10]。所以,本例中的问题是数组越界,导致程序运行时报错。
[正确代码]
代码修改如下:
5 |
for(int i=0;i<10;i++) |
6 |
{ |
7 |
a[i]
= i; |
8 |
} |
9 |
for(i=9;i>=0;i--) |
[运行结果]
9 8 7 6 5 4 3 2 1 0
[例8.2] 输入学生数量,再输入每个学生的成绩,判断是否及格。
[错误代码]
1 |
#include "stdio.h" |
2 |
#include "stdlib.h" |
3 |
void main( ) |
4 |
{ |
5 |
int n; |
6 |
printf("请输入学生数量:"); |
7 |
scanf("%d", &n); |
8 |
|
9 |
int a[n]; |
10 |
for(int i=0;i<n;i++) |
11 |
{ |
12 |
printf("请输入第%d个学生成绩:", i+1); |
13 |
scanf("%d", &a[i]); |
14 |
if(a[i]>=60) |
15 |
printf("及格!\n"); |
16 |
else |
17 |
printf("不及格!\n"); |
18 |
} |
19 |
} |
[编译结果]
E:\CODE\8_2.cpp(9)
: error C2057: expected constant expression
E:\CODE\8_2.cpp(9)
: error C2466: cannot allocate an array of constant size 0
E:\CODE\8_2.cpp(9)
: error C2133: 'a' : unknown size
[问题分析]
数组名后用方括号括起来的是常量表达式,可以包括常量和符号常量。即C不允许对数组的大小作动态定义。可以用malloc动态分配内存,最后用free释放内存。所以,把第9行代码
int a[n];
改为:
int* a =
(int*)malloc(n*sizeof(int));
最后用
free(a);
释放内存。
另外,如果内存不够,malloc函数会返回一个空指针来表示“内存分配失败”。所以,一定要判断返回指针是否为空指针,才能访问该指针。如果内存分配失败时没有判断返回指针是否为空指针,就对其进行操作的话,会导致程序出错。
[正确代码]
第8行之后的代码改为:
9 |
int* a =
(int*)malloc(n*sizeof(int)); |
10 |
if(a) |
11 |
{ |
12 |
for(int i=0;i<n;i++) |
13 |
{ |
14 |
printf("请输入第%d个学生成绩:", i+1); |
15 |
scanf("%d", &a[i]); |
16 |
if(a[i]>=60) |
17 |
printf("及格!\n"); |
18 |
else |
19 |
printf("不及格!\n"); |
20 |
} |
21 |
free(a); |
22 |
} |
23 |
} |
[运行结果]
请输入学生数量:3
请输入第1个学生成绩:89
及格!
请输入第2个学生成绩:40
不及格!
请输入第3个学生成绩:60
及格!
[例8.3] 已知一个字符数组s,为另一个字符数组t动态分配空间,把s的内容复制到t。
[错误代码]
1 |
#include "stdio.h" |
2 |
#include "stdlib.h" |
3 |
#include "string.h" |
4 |
void main( ) |
5 |
{ |
6 |
char s[] = "hello!"; |
7 |
char* t = (char*)malloc(strlen(s)); |
8 |
strcpy(t,s); |
9 |
printf("%s\n",t); |
10 |
free(t); |
11 |
} |
[编译结果]
编译通过,运行时虽然输出结果正确,但程序报错:
图8.2动态分配内存空间不够
[问题分析]
使用strcpy和strcat对字符数组进行操作,必须为其预留足够的空间。否则虽然程序编译通过,但会导致字符串操作数组越界,从而导致潜在的程序错误。本例中为t分配的空间大小是strlen(s),看起来正好够用。但实际上字符串是以空字符‘\
另外,如果使用malloc分配的空间必须用free释放。如果忘记释放(例如在本例中去掉free(t);这一行代码),可能不会弹出上图中的错误窗口,但会导致潜在的程序错误。
[正确代码]
代码修改为:
7 |
char* t = (char*)malloc(strlen(s)+1); |
[运行结果]
hello!
动态分配的内存空间必须释放。如果忘记释放,则系统无法把这些内存再分配给应用程序使用,称为内存泄露。内存泄露会导致系统运行速度变慢,严重时甚至死机。
[例8.4] 循环输入你喜爱的水果,输入exit则退出。
[错误代码]
1 |
#include "stdio.h" |
2 |
#include "stdlib.h" |
3 |
#include "string.h" |
4 |
char* GetName( ) |
5 |
{ |
6 |
char* s = (char*)malloc(20); |
7 |
printf("请输入你喜爱的水果:"); |
8 |
scanf("%s",s); |
9 |
return s; |
10 |
} |
11 |
void main( ) |
12 |
{ |
13 |
char *p = NULL; |
14 |
while(1) |
15 |
{ |
16 |
p = GetName( ); |
17 |
if(!strcmp(p,"exit")) |
18 |
{ |
19 |
break; |
20 |
} |
21 |
printf("你喜欢吃%s。\n",p); |
22 |
} |
23 |
} |
[编译结果]
编译正常,运行结果也正常。
[问题分析]
动态分配的内存必须显示地释放。如果一个程序需要连续很多天、每天24小时运行(例如工厂里的生产线自动控制程序),那么即使只有很少的内存泄漏,也会很快耗尽内存导致死机。本例中,在函数GetName( )中用malloc动态分配内存,返回char*指针,却没有释放内存,导致内存泄漏。应该在循环结束的时候,用free(p) 释放内存。注意,如果用户输入exit,则应该用break结束循环,这时候不要忘记用free(p) 释放内存,否则仍有内存泄漏。
[正确代码]
第17行及之后的代码改为:
17 |
if(!strcmp(p,"exit")) |
18 |
{ |
19 |
free(p); |
20 |
break; |
21 |
} |
22 |
printf("你喜欢吃%s。\n",p); |
23 |
free(p); |
|
} |
|
} |
[运行结果]
请输入你喜爱的水果:苹果
你喜欢吃苹果。
请输入你喜爱的水果:榴莲
你喜欢吃榴莲。
请输入你喜爱的水果:exit
[例8.5] 访问悬空指针。
[错误代码]
1 |
#include "stdio.h" |
2 |
#include "stdlib.h" |
3 |
#include "string.h" |
4 |
void main( ) |
5 |
{ |
6 |
char* s = (char*)malloc(10); |
7 |
char* t = s; |
8 |
strcpy(s,"hello!"); |
9 |
printf("s:%s\n",s); |
10 |
printf("t:%s\n",t); |
11 |
free(s); |
12 |
s=NULL; |
13 |
printf("free s.\n"); |
14 |
printf("s:%s\n",s); |
15 |
printf("t:%s\n",t); |
16 |
} |
[编译结果]
编译通过,但运行结果有问题:
s:hello!
t:hello!
free s.
s:(null)
t:葺葺葺葺葺葺葺
[问题分析]
本例中指针s指向malloc分配的内存空间。语句char* t = s使t也指向这个空间。free(s)之后,实际上s和t都变成了悬空指针。s=NULL语句把s变成空指针,却忘记了指针t. 此后,程序中访问了指针t,结果出现了莫名其妙的结果,输出了“t:葺葺葺葺葺葺葺”。
使用指针时一定要避免指针定义后未赋值就引用。如果在定义时不知道赋什么值,可以用p=NULL赋初值,以避免引起的灾难性错误。在释放指针所指向的内存空间后,记得把该指针变量赋为NULL,避免访问悬空指针带来的灾难性错误。
[正确代码]
在第14行处插入一行代码:
14 |
t=NULL; |
[运行结果]
s:hello!
t:hello!
free s.
s:(null)
t:(null)
请改正如下程序中的错误。
1) 求一个3*3矩阵对角线元素之和。
1 |
#include <stdio.h> |
2 |
void main( ) |
3 |
{ |
4 |
int n=3; |
5 |
float a[n][n],sum=0; |
6 |
int i,j; |
7 |
printf("请输入矩阵元素:\n"); |
8 |
for(i=1;i<=3;i++) |
9 |
for(j=1;j<=3;j++) |
10 |
scanf("%f",&a[i][j]); |
11 |
for(i=1;i<=3;i++) |
12 |
sum=sum+a[i][i]; |
13 |
printf("矩阵对角线元素之和是:% |
14 |
} |
2) 动态分配内存,复制一个已知的整型数组。
1 |
#include <stdio.h> |
2 |
#include <stdlib.h> |
3 |
int* CloneArray(int* pArray, int len); |
4 |
void main( ) |
5 |
{ |
6 |
int x[3] = {2,3,4}; |
7 |
int* pNewArray = CloneArray(x,3); |
8 |
if(!pNewArray) return; |
9 |
for(int i=0;i<3;i++) |
10 |
printf("%d\t",pNewArray[i]); |
11 |
printf("\n"); |
12 |
} |
13 |
int* CloneArray(int* pArray, int len) |
14 |
{ |
15 |
int* p = (int*)malloc(len); |
16 |
for(int i=0;i<3;i++) |
17 |
p[i] = pArray[i]; |
18 |
return p; |
19 |
} |
1.结构
“结构”是一种构造类型,是由若干“成员”组成的。 每一个成员可以是一个基本数据类型或者是另一个构造类型。 结构在说明和使用之前必须先定义。定义结构变量的一般格式为:
struct 结构名
{
数据类型 成员名;
数据类型 成员名;
...
} 结构变量;
结构名是结构的标识符。结构成员的类型可以为整型、浮点型、字符型、指针型等数据类型,或者是另一个构造类型。例如:
struct student
{
int num;
char name[20];
char sex;
float score;
} st;
构成结构的每一个类型变量称为结构成员,它象数组的元素一样,但数组中元素是以下标来访问的,而结构是按变量名字来访问成员的。结构是一种新的数据类型,同样可以有结构数组和结构指针。结构数组就是具有相同结构类型的变量集合。结构指针是指向结构的指针。它由一个加在结构变量名前的"*" 操作符来定义。使用时需注意以下几点:
定义的结构变量或结构指针变量同样有局部变量和全程变量之分。
与数组名的含义不同,结构变量名不是指向该结构的地址,因此若需要求结构中第一个成员的首地址应该是&[结构变量名]。
结构可以嵌套
2.联合
联合是一种特殊形式的变量,表示几个变量共用一块内存,在不同的时间保存不同的数据类型和不同长度的变量。联合说明和联合变量定义与结构十分相似。其形式为:
union 联合名{
数据类型 成员名;
数据类型 成员名;
...
} 联合变量名;
例如:
union data{
int i;
char mm;
} d;
在联合变量d中,整型量i和字符mm共用同一内存空间。
结构和联合有下列区别:
(1)结构和联合都是由多个不同的数据类型成员组成,但在任何同一时刻,联合中只存放了一个被选中的成员,而结构的所有成员都存在。
(2)对于联合的不同成员赋值,将会对其它成员重写,原来成员的值就不存在了,而对于结构的不同成员赋值是互不影响的。
[例9.1] 定义结构STUDENT,存储学生姓名和生日(结构date嵌套在STUDENT里)。
[错误代码]
1 |
#include "stdio.h" |
2 |
#include "string.h" |
3 |
struct STUDENT |
4 |
{ |
5 |
char name[20]; |
6 |
struct date |
7 |
{ |
8 |
int year,month,day; |
9 |
}birthday |
10 |
} |
11 |
void main( ) |
12 |
{ |
13 |
STUDENT tom; |
14 |
strcpy(tom.name, "Tom"); |
15 |
tom.year = 1990; |
16 |
tom.month = 5; |
17 |
tom.day = 4; |
18 |
|
19 |
printf("姓名:%s\n",tom.name); |
20 |
printf("生日:%d年%d月%d日\n",tom.year,tom.month,tom.day); |
21 |
} |
[编译结果]
E:\CODE\9_1.cpp(10)
: error C2143: syntax error : missing ';' before '}'
E:\CODE\9_1.cpp(11)
: error C2628: 'STUDENT' followed by 'void' is illegal (did you forget a ';'?)
E:\CODE\9_1.cpp(15)
: error C2039: 'year' : is not a member of 'STUDENT'
E:\CODE\9_1.cpp(4) : see declaration of 'STUDENT'
E:\CODE\9_1.cpp(16)
: error C2039: 'month' : is not a member of 'STUDENT'
E:\CODE\9_1.cpp(4) : see declaration of 'STUDENT'
E:\CODE\9_1.cpp(17)
: error C2039: 'day' : is not a member of 'STUDENT'
E:\CODE\9_1.cpp(4) : see declaration of 'STUDENT'
E:\CODE\9_1.cpp(20)
: error C2039: 'year' : is not a member of 'STUDENT'
E:\CODE\9_1.cpp(4) : see declaration of 'STUDENT'
E:\CODE\9_1.cpp(20)
: error C2039: 'month' : is not a member of 'STUDENT'
E:\CODE\9_1.cpp(4) : see declaration of 'STUDENT'
E:\CODE\9_1.cpp(20)
: error C2039: 'day' : is not a member of 'STUDENT'
E:\CODE\9_1.cpp(4) : see declaration of 'STUDENT'
E:\CODE\9_1.cpp(21)
: warning C4508: 'main' : function should return a value; 'void' return type
assumed
[问题分析]
本例中犯了两个错误:
1)定义结构时,在“}”后要加分号。第9行代码birthday后面和第10 行代码“}”后面都忘了写分号。编译错误信息的第1、2条就指明了这个错误。
2)结构类型定义有误,主要表现在:结构类型里还有嵌套的时候,忘记了成员名称。例如:本例中,忽略了birthday。应该写成tom.birthday.year、tom.birthday.month、tom.birthday.day,而不是tom.year, tom.month, tom.day。
[正确代码]
第9行及之后代码改为:
9 |
}birthday; |
10 |
}; |
11 |
void
main( ) |
12 |
{ |
13 |
STUDENT tom; |
14 |
strcpy(tom.name,
"Tom"); |
15 |
tom.birthday.year
= 1990; |
16 |
tom.birthday.month
= 5; |
17 |
tom.birthday.day
= 4; |
18 |
|
19 |
printf("姓名:%s\n",tom.name); |
20 |
printf("生日:%d年%d月%d日\n",tom.birthday.year,
tom.birthday.month,tom.birthday.day); |
21 |
} |
[运行结果]
姓名:Tom
生日:
[例9.2] 定义一个联合,可存储一个整数,或一个字符,或一个浮点数。
[错误代码]
1 |
#include "stdio.h" |
2 |
union data |
3 |
{ |
4 |
int i; |
5 |
char ch; |
6 |
float f; |
7 |
}; |
8 |
void main( ) |
9 |
{ |
10 |
data a; |
11 |
a.i = 5; |
12 |
a.ch = 'A'; |
13 |
a.f = |
14 |
printf("整数:%d\n",a.i); |
15 |
printf("字符:%c\n",a.ch); |
16 |
printf("浮点数:% |
17 |
} |
[编译结果]
编译通过,但运行结果有问题:
整数:1074203197
字符:=
浮点数:2.11
输出的整数值并非先前赋值的5,输出的字符值也不是非先前赋值的'A'。
[问题分析]
使用联合时要注意:
1)
同一个内存段可以用来存放几种不同类型的成员,但在每一瞬时只能存放其中一种,而不是同时存放几种。也就是说,每一瞬时只有一个成员起作用,其他的成员不起作用,即不是同时都存在和起作用。
2)
联合变量中起作用的是最后一次存放的成员,在存入一个新的成员后原有的成员就失去作用。
3)
联合变量的地址和它的各成员的地址都是同一地址。
4)
不能对联合变量名赋值,也不能企图引用变量名来得到成员的值,又不能在定义联合变量时对它初始化。
5)
不能把联合变量作为参数,也不能使函数带回联合变量,但可以使用指向联合变量的指针。
6)
联合类型可以出现在结构类型定义中,也可以定义联合数组。反之,结构也可以出现在联合类型定义中,数组也可以作为联合的成员。
7) 在本例中,混淆了结构与联合,误以为联合也可以象结构那样可以同时为几个成员赋值。实际上只有最后一次被赋值的成员起作用。输出的整数成员和字符成员都是错的。
[正确代码]
去掉无效的代码:
11 |
//a.i = 5; |
12 |
//a.ch = 'A'; |
14 |
//printf("整数:%d\n",a.i); |
15 |
//printf("字符:%c\n",a.ch); |
[运行结果]
浮点数:2.11
请改正如下程序中的错误。
1) 定义结构Circle,存储圆心坐标和半径。其中圆心坐标定义为结构Location,并嵌套在结构Circle中。
1 |
#include "stdio.h" |
2 |
struct Circle |
3 |
{ |
4 |
struct Location |
5 |
{ |
6 |
int x,y; |
7 |
}center; |
8 |
int radius |
9 |
} |
10 |
void main( ) |
11 |
{ |
12 |
Circle c; |
13 |
c.x = 5; |
14 |
c.y = 4; |
15 |
c.radius = 3; |
16 |
|
17 |
printf("圆心坐标:(%d,%d)\n", c.x, c.y); |
18 |
printf("半径:%d\n", c.radius); |
19 |
} |
2) 定义联合,可存储两个整型数,或一个长整型数,或4个char字符。
1 |
#include "stdio.h" |
2 |
#include "string.h" |
3 |
union data |
4 |
{ |
5 |
int i[2]; |
6 |
long l; |
7 |
char c[4]; |
8 |
}; |
9 |
void main( ) |
10 |
{ |
11 |
data dt; |
12 |
dt.i[0] = 5; |
13 |
dt.i[1] = 6; |
14 |
dt.l = 100; |
15 |
strcpy(dt.c, "book"); |
16 |
printf("整数:%d,%d\n",dt.i[0],dt.i[1]); |
17 |
printf("长整数:%d\n",dt.l); |
18 |
printf("字符数组:%s\n",dt.c); |
19 |
} |
在程序的运行过程中,往往需要由用户输入一些数据,而程序运算所得到的计算结果等又需要输出给用户,由此实现人与计算机之间的交互。在C语言中,输入输出操作通过调用标准的I/O库函数实现。使用时需要包含头文件stdio.h 。
1. 字符的输入和输出——getchar和putchar
这两个函数用来输入和输出字符,其格式为:
int getchar(void)
int putchar(int ch)
getchar主要是从标准输入流读取一个字符,默认的标准输入流即stdio.h中定义的stdin。putchar(ch)主要是把字符ch写到标准输出流stdout中去.
2. 格式化输入和输出——scanf 和printf
printf函数是格式化输出函数,一般用于向标准输出设备按规定格式输出信息。printf函数的调用格式为:
int printf("<格式化字符串>", <参量表>);
其中格式化字符串包括正常字符和格式化规定字符(以“%”开始,后跟一个或几个规定字符)。正常字符将按原样输出,而格式化规定字符用来确定输出内容格式。 参量表是需要输出的一系列参数,其个数必须与格式化字符串所说明的输出参数个数一样多,各参数之间用“,”分开,且顺序一一对应,否则将会出现意想不到的错误。
scanf 函数是格式化输入函数,功能与printf函数类似,只不过方向相反。scanf 函数从标准输入流中读取数据。其格式为:
int scanf("<格式化字符串>", <参量地址表>);
其中格式化字符串为指定的参数格式及参数类型,如scanf("%s,%d",str, &count);
3. 字符串的输入和输出——gets和puts
gets主要是从标准输入流读取字符串并回显,读到换行符时退出,并会将换行符省去。其格式为:
char * gets(char *str)
puts主要是把字符串str写到标准流stdout中去,并会在输出到最后时添加一个换行符。其格式为:
int puts(char *str)
4. 文件
所谓“文件”是指一组相关数据的有序集合。从文件编码的方式来看,文件可分为ASCII码文件和二进制码文件两种。ASCII文件也称为文本文件,这种文件在磁盘中存放时每个字符对应一个字节,用于存放对应的ASCII码。二进制文件是按二进制的编码方式来存放文件的。对文件的操作有打开、关闭、读、写、定位等。在C语言中,文件操作都是由库函数来完成的,下面简单介绍一下主要的文件操作函数。
(1)文件打开函数fopen
fopen函数用来打开一个文件,其调用的一般形式为:
文件指针名 = fopen(文件名,使用文件方式)
其中,“文件指针名”必须是被说明为FILE 类型的指针变量,“文件名”是被打开文件的路径和文件名。 “使用文件方式”是指文件的类型和操作要求。“文件名”是字符串常量或字符串数组。例如:
FILE *fp = ("c://readme.txt","r");
其意义是打开C盘根目录下文件readme.txt,只允许进行“读”操作,并使fp指向该文件。
(2)文件关闭函数fclose
文件一旦使用完毕,应把文件关闭,以避免文件的数据丢失等错误。 fclose函数调用的一般形式是:
fclose(文件指针);
例如:
fclose(fp);
正常完成关闭文件操作时,fclose函数返回值为0。如返回非零值则表示有错误发生。
(3)文件的读写
对文件的读和写是最常用的文件操作。在C语言中提供了多种文件读写的函数:
·字符读写函数 :fgetc和fputc
·字符串读写函数:fgets和fputs
·数据块读写函数:freed和fwrite
·格式化读写函数:fscanf和fprinf
[例10.1] 统计候选人得票数。
[错误代码]
1 |
#include "stdio.h" |
2 |
#include "string.h" |
3 |
struct person |
4 |
{ |
5 |
char name[20]; //候选人姓名 |
6 |
int count; //候选人得票数 |
7 |
}leader[4]={"Zhao",0,"Qian",0,"Sun",0,"Li",0}; |
8 |
void main( ) |
9 |
{ |
10 |
char leader_name[20]; |
11 |
printf("开始投票:\n"); |
12 |
for(int i=0;i<10;i++) |
13 |
{ |
14 |
scanf("%s", &leader_name); //投票 |
15 |
|
16 |
for(int j=0;j<4;j++) |
17 |
if(!strcmp(leader_name, leader[j].name)) |
18 |
leader[j].count++; |
19 |
} |
20 |
|
21 |
printf("投票结果:\n"); |
22 |
for(int k=0;k<4;k++) |
23 |
printf("%d:\t票\n",leader[k].name,leader[k].count); |
24 |
} |
[编译结果]
编译通过,但运行结果有问题:
开始投票:
Zhao
Qian
Qian
Sun
Qian
Li
Li
Zhao
Zhao
Zhao
投票结果:
4355584: 票
4355608: 票
4355632: 票
4355656: 票
候选人姓名和候选人得票数都没有正常显示。
[问题分析]
printf( )和scanf( )的参数设置有误,主要表现在以下几方面:
1) 类型不匹配的问题。(例如:有float a=3.5,但输出的时候如果写成printf(“a=%d”,a)则屏幕上会显示出a=0或者提示其它运行错误)。基本原则是:float对应%f,int对应%d,char对应%c。在本例中,倒数第2行代码中leader[k].name对应的应该是%s,而非%d。
2) 个数不匹配。无论是哪个格式化输入/输出函数,都可以有n个参数,第一个永远是“ ”括起来的内容,表示输出格式。剩下的n-1个是输出的变量或者输入的变量的地址。需要注意的是,如果后边有n-1个参数,那么前边格式串中一定有对应n-1个类似“%f”的格式说明符。在本例中,倒数第2行代码中leader[k]. count对应的%d被遗漏了。
3)
scanf( )中变量前忘了加&。scanf( )中变量前要有&(字符数组名和指针前不用加) 。如:
int a,b;
scanf("%d%d",a,b);
这是不合法的。scanf函数的作用是:按照a、b在内存的地址将a、b的值存进去。“&a”指a在内存中的地址。在本例中,第14行代码中leader_name是字符数组,前面不用加“&”。
[正确代码]
代码修改为:
14 |
scanf("%s", leader_name); //投票 |
以及:
23 |
printf("%s:\t%d票\n",leader[k].name,leader[k].count); |
[运行结果]
开始投票:
Zhao
Qian
Qian
Sun
Qian
Li
Li
Zhao
Zhao
Zhao
投票结果:
Zhao: 4票
Qian: 3票
Sun: 1票
Li: 2票
[例10.2] 打开C盘根目录的文件file1.txt,读取其内容,并写入C盘根目录的文件file2.txt。
[错误代码]
1 |
#include "stdio.h" |
2 |
#include "stdlib.h" |
3 |
void main( ) |
4 |
{ |
5 |
FILE* in = fopen("c://file1.txt","wt"); |
6 |
FILE* out; |
7 |
char ch; |
8 |
while((ch=fgetc(in)) != EOF) |
9 |
{ |
10 |
fputc(ch,out); |
11 |
printf("%c",ch); |
12 |
} |
13 |
printf("\n文件复制成功!\n"); |
14 |
fclose(in); |
15 |
} |
[编译结果]
e:\code\10_2.cpp(11)
: warning C4700: local variable 'out' used without having been initialized
但运行时,没有产生file2.txt,而且file1.txt的内容反而被清空。
[问题分析]
C语言中文件的操作常常会出现如下错误:
1) 使用之前没有打开文件,使用之后没有关闭文件。
2) 文件打开模式错误。
3) 没有检查文件是否打开失败,就进行读写操作。
代码编译时只有一个警告:文件指针out没有初始化。实际上是使用前忘了打开或者创建文件。另外,文件指针in的打开模式应该是"rt",而不是"wt"。当文件的路径错误,或者文件不存在时,文件打开可能失败,如果没有检查文件是否打开失败,就进行读写操作,可能会导致程序意外终止。另外,文件指针out没有被关闭。
[正确代码]
1 |
#include "stdio.h" |
2 |
#include "stdlib.h" |
3 |
void main( ) |
4 |
{ |
5 |
FILE* in = fopen("c://file1.txt","rt"); |
6 |
FILE* out =
fopen("c://file2.txt","wt"); |
7 |
if(in==NULL
|| out==NULL) |
8 |
{ |
9 |
printf("文件打开失败!\n"); |
10 |
return; |
11 |
} |
12 |
char ch; |
13 |
while((ch=fgetc(in)) != EOF) |
14 |
{ |
15 |
fputc(ch,out); |
16 |
printf("%c",ch); |
17 |
} |
18 |
printf("\n文件复制成功!\n"); |
19 |
fclose(out); |
20 |
fclose(in); |
21 |
} |
[运行结果]
Hello,world!
文件复制成功!
请改正如下程序中的错误。
1) 定义结构NoteBook,保存笔记本电脑的品牌和价格。
1 |
#include "stdio.h" |
2 |
#include "string.h" |
3 |
struct NoteBook |
4 |
{ |
5 |
char brand[20]; |
6 |
float price; |
7 |
}notebook; |
8 |
void main( ) |
9 |
{ |
10 |
scanf("%s", ¬ebook.brand); |
11 |
scanf("%f", ¬ebook.price); |
12 |
printf("品牌:%d\n" ,notebook.brand); |
13 |
printf("价格:%f\n" ,notebook.price); |
14 |
} |
2) 把及格学生和不及格学生的姓名和分数分别写到不同的文件中。
1 |
#include "stdio.h" |
2 |
#include "stdlib.h" |
3 |
struct STUDENT |
4 |
{ |
5 |
char name[20]; |
6 |
int score; |
7 |
}student[4]={"Zhao",50,"Qian",80,"Sun",90,"Li",33}; |
8 |
void main( ) |
9 |
{ |
10 |
FILE* fp1 = fopen("pass.txt","rt"); |
11 |
FILE* fp2; |
12 |
for(int i=0;i<4;i++) |
13 |
{ |
14 |
if(student[i].score<60) |
15 |
fprintf(fp2,"%s,%d\n",student[i].name,student[i].score); |
16 |
else |
17 |
fprintf(fp1,"%s,%d\n",student[i].name,student[i].score); |
18 |
} |
19 |
fclose(fp2); |
20 |
} |
本部分主要包括命名空间、C++语言的输入输出、动态内存的分配与释放、引用、const修饰符、字符串、C++语言中函数的新特性等内容。
命名空间是随标准C++而引入的。它相当于一个更加灵活的文件域(全局域),可以用大括号把文件的一部分括起来,并以关键字namespace开头给它起一个名字,如:
namespace mynamespace
{
int number;
Add( ){……}
}
大括号括起来的部分称声明块。声明块中可以包括:类、变量(带有初始化)、函数(带有定义)等。在域外使用域内的成员时,需加上命名空间名作为前缀,后面加上域操作符“::”,例如mynamespace::number。
命名空间可分层嵌套,例如:
namespace out{
namespace in { // 命名空间嵌套
class Student{……} // 命名空间类成员Student
}
}
访问Student,可以使用out::in::Student。
如果使用using
out::in::Student声明Student,则以后在程序中使用Studen时,就不必使用限定修饰名。使用using指示符可以一次性地使命名空间中所有成员都可以直接被使用,如using namespace mynamespace。
标准C++库中的所有组件都是在一个被称为std的命名空间中声明和定义的。在采用标准C++的平台上使用标准C++库中的组件,只要写一个using指示符:
using namespace std;
就可以直接使用标准C++库中的所有成员。如果使用了名空间std,则在使用#include编译预处理命令包含头文件时,必须去掉头文件的扩展名.h,否则会出错。
[例11.1] 使用C++标准程序库的命名空间std,输出“Hello,world!”。
[错误代码]
1 |
#include <iostream.h> |
2 |
using namespace std; |
3 |
void main( ) |
4 |
{ |
5 |
cout<<"Hello,world!"<<endl; |
6 |
} |
[编译结果]
E:\CODE\11_1.cpp(2)
: error C2871: 'std' : does not exist or is not a namespace
[问题分析]
C++标准程序库中的所有标识符都被定义于一个名为std的namespace中。<iostream>和<iostream.h>是不一样,前者没有.h后缀,实际上,二者是两个文件,打开文件就会发现,里面的代码是不一样的。
C++标准已经明确提出不支持后缀为.h的头文件,早些的实现将标准库功能定义在全局空间里,声明在带.h后缀的头文件里。C++标准为了和C区别开,也为了正确使用命名空间,规定头文件不使用后缀.h。
因此,当使用<iostream.h>时,相当于在C中调用库函数,使用的是全局命名空间,也就是早期的C++实现;当使用<iostream>的时候,该头文件没有定义全局命名空间,必须使用namespace std;这样才能正确使用cout。
[正确代码]
代码修改为:
1 |
#include <iostream> //去掉.h |
[运行结果]
Hello,world!
[例11.2] class1.h和class2.h中都定义了MyClass类,在主程序中同时使用产生了名字冲突。
[错误代码]
class1.h
1 |
#include <iostream> |
2 |
using namespace std; |
3 |
class MyClass |
4 |
{ |
5 |
public: |
6 |
void Info( ) |
7 |
{ |
8 |
cout<<"这是class1.h"<<endl; |
9 |
} |
10 |
}; |
class2.h
1 |
#include <iostream> |
2 |
using namespace std; |
3 |
class MyClass |
4 |
{ |
5 |
public: |
6 |
void Info( ) |
7 |
{ |
8 |
cout<<"这是class2.h"<<endl; |
9 |
} |
10 |
}; |
11_2.cpp
1 |
#include "class1.h" |
2 |
#include "class2.h" |
3 |
#include <iostream> |
4 |
using namespace std; |
5 |
void main( ) |
6 |
{ |
7 |
//声明一个文件class1.h中类MyClass的实例x |
8 |
MyClass x; |
9 |
//声明一个文件class2.h中类MyClass的实例x |
10 |
MyClass y; |
11 |
x.Info( ); |
12 |
y.Info( ); |
13 |
} |
[编译结果]
e:\code\11_2\class2.h(4)
: error C2011: 'MyClass' : 'class' type redefinition
[问题分析]
因为class1.h和class2.h里都定义了类MyClass,导致名字冲突,编译器认为类MyClass被重复定义。一种办法是将其中的一个类名改掉;另一种方法是在class1.h和class2.h里都引入namespace。
[正确代码]
Class1.h修改如下:
1 |
#include
<iostream> |
2 |
using
namespace std; |
3 |
namespace
MyNamespace1 |
4 |
{ |
5 |
class MyClass |
6 |
{ |
7 |
public: |
8 |
void Info( ) |
9 |
{ |
10 |
cout<<"这是class1.h"<<endl; |
11 |
} |
12 |
}; |
13 |
}; |
Class2.h修改如下:
1 |
#include
<iostream> |
2 |
using
namespace std; |
3 |
namespace
MyNamespace2 |
4 |
{ |
5 |
class MyClass |
6 |
{ |
7 |
public: |
8 |
void Info( ) |
9 |
{ |
10 |
cout<<"这是class2.h"<<endl; |
11 |
} |
12 |
}; |
13 |
}; |
11_2.cpp修改如下:
8 |
MyNamespace1::MyClass
x; |
10 |
MyNamespace2::MyClass
y; |
[运行结果]
这是class1.h
这是class2.h
请改正如下程序中的错误。
1) 对输入的整数判断是正数还是负数。输入0则退出。
1 |
#include <iostream.h> |
2 |
using namespace std; |
3 |
void main( ) |
4 |
{ |
5 |
int data; |
6 |
while(1) |
7 |
{ |
8 |
cin>>data; |
9 |
if(data>0) |
10 |
cout<<data<<"是正数。"<<endl; |
11 |
else if(data<0) |
12 |
cout<<data<<"是负数。"<<endl; |
13 |
else |
14 |
{ |
15 |
cout<<data<<"既不是正数,也不是负数。"<<endl; |
16 |
break; |
17 |
} |
18 |
|
19 |
} |
20 |
} |
2) 编程将输入的英寸值转换成厘米值(使用名字空间的嵌套)。
1 |
#include <iostream.h> |
2 |
namespace n1{ |
3 |
namespace n2 { // 名字空间嵌套 |
4 |
float InchToCM(float inch) |
5 |
{ |
6 |
return 2.54*inch; |
7 |
} |
8 |
} |
9 |
} |
10 |
|
11 |
void main( ) |
12 |
{ |
13 |
int inch; |
14 |
cin>>inch; |
15 |
cout<<inch<<"英寸等于"<<InchToCM(inch)<<"厘米。"<<endl; |
16 |
} |
C++语言定义了一套保留字与运算符来代替C语言的标准输入、输出函数。C++语言中的输出和输入的一般格式为:
cout<<“输出内容”<<…; // cout为标准输出流对象(默认输出到显示器)
cin>>“输入内容”>>…;
// cin为标准输入流对象(默认从键盘输入)
cout和cin这两个流对象的内部函数在iostream.h中声明。
[例12.1] 输入华氏温度,输出摄氏温度(C=(F-32)*5/9)。
[错误代码]
1 |
#include <iostream.h> |
2 |
#include <iomanip.h> |
3 |
void main( ) |
4 |
{ |
5 |
double f,c; |
6 |
cout>>"请输入华氏温度:"; |
7 |
cin<<f; |
8 |
c = (f-32)*5.0/9.0; |
9 |
cout<<"摄氏温度:"<<setprecision(4)<<c<<endl; |
10 |
} |
[编译结果]
E:\CODE\12_1.cpp(6)
: error C2678: binary '>>' : no operator defined which takes a left-hand
operand of type 'class ostream_withassign' (or there is no acceptable
conversion)
E:\CODE\12_1.cpp(7)
: error C2678: binary '<<' : no operator defined which takes a left-hand
operand of type 'class istream_withassign' (or there is no acceptable
conversion)
[问题分析]
在使用cin和cout对象时,要注意箭头的方向。本例中第6、7行代码,“>>”、“<<”都弄反了。
[正确代码]
代码修改如下:
6 |
cout<<"请输入华氏温度:"; |
7 |
cin>>f; |
[运行结果]
请输入华氏温度:100
摄氏温度:37.78
[例12.2] 输入一个分数,判断是否及格,并输出。
[错误代码]
1 |
#include "stdio.h" |
2 |
#include "iostream.h" |
3 |
void main( ) |
4 |
{ |
5 |
int score; |
6 |
printf("请输入分数:"); |
7 |
scanf("%d",&score); |
8 |
cout << score>=60 ? "及格" : "不及格" << endl; |
9 |
} |
[编译结果]
E:\CODE\12_2.cpp(8)
: error C2676: binary '>=' : 'class ostream' does not define this operator
or a conversion to a type acceptable to the predefined operator
E:\CODE\12_2.cpp(8)
: error C2296: '<<' : illegal, left operand has type 'char [7]'
E:\CODE\12_2.cpp(8)
: error C2297: '<<' : illegal, right operand has type 'class ostream
&(__cdecl *)(class ostream &)'
[问题分析]
第8行代码里使用了三目运算符。它本身并没有问题,但它的优先级比左移运算符要低。所以,按照编译器的理解,我们是企图让cout左移score位,然后再把这个结果用作三目运算符所需的判别表达式。要解决这个问题,只要加括号即可:
cout <<
(score>=60 ? "及格" : "不及格" )<< endl;
或者
if(score>=60)
cout << "及格"<< endl;
else
cout << "不及格"<< endl;
[正确代码]
代码修改如下:
8 |
cout << (score>=60 ? "及格" : "不及格" )<< endl; |
[运行结果]
请输入分数:65
及格
请改正如下程序中的错误。
1) 输入两个整数,输出它们的平方和。
1 |
#include <iostream.h> |
2 |
void main( ) |
3 |
{ |
4 |
int m,n; |
5 |
cout>>"请输入两个整数:"; |
6 |
cin<<m; |
7 |
cin<<n; |
8 |
int result = m*m+n*n; |
9 |
cout>>"它们的平方和是:">>result>>endl; |
10 |
} |
2) 输入商品剩余数量,如果少于50件,则需采购,将其补足到150个。输出需采购数量。
1 |
#include <iostream.h> |
2 |
void main( ) |
3 |
{ |
4 |
int count; |
5 |
cout<<"请输入商品剩余数量:"; |
6 |
cin>>count; |
7 |
cout<<"需采购"<<count<50 ? 150-count : 0<<"件。"<<endl; |
8 |
} |
在软件开发中,经常需要动态地分配与释放内存。在程序运行时可使用的内存空间称为堆(heap)。new运算符可以为程序分配可以保存某种类型数据的一块内存空间,并返回指向该内存的首地址,该地址存放于指针变量中。使用形式为:
指针变量 = new 数据类型;
当不能成功地分配到所需要的内存时,new返回0,即空指针。随着程序的运行,由new分配的内存空间可能会不再需要,这时可以用delete运算符把先前占用的内存空间释放,以重新分配,供程序的其它部分使用。delete运算符的使用形式为:
delete 指针变量;
其中的指针变量保存着new动态分配的内存的首地址。注意:
(1) 用new获取的内存空间,必须用delete进行释放;
(2) 对一个指针只能调用一次delete;
(3) 用delete运算符作用的对象必须是用new分配的内存空间的首地址。
new/delete和malloc/free都可以动态地分配与释放内存。但二者有一些区别:
(1) new/delete是C++中的运算符,malloc/free是C中的一个函数;
(2) new 不但分配内存,还调用类的构造函数,delete会调用类的析构函数;而malloc则只分配内存,不会进行初始化类成员的工作,同样free也不会调用析构函数;
(3) 内存泄漏对于malloc或者new都可以检查出来的,区别在于new可以指明是哪个文件的哪一行,而malloc没有这些信息。
(4) new返回的指针是带类型信息的,而malloc返回的都是void指针。
所以动态对象的内存管理,应该用new/delete,而不要用malloc/free。内部数据类型变量没有构造与析构的过程,对它们而言malloc/free和new/delete是等价的。
new/delete必须配对使用,malloc/free也一样。如果用free释放用new创建的动态对象,那么该对象因无法执行析构函数而可能导致程序出错。如果用delete释放用malloc申请的动态内存,理论上不会出错,但是该程序的可读性很差。
[例13.1] 定义学生类,有学号和姓名属性,在构造函数里为姓名分配内存空间,在析构函数里释放为姓名分配的内存空间。
[错误代码]
1 |
#include <stdlib.h> |
2 |
#include <string.h> |
3 |
#include <iostream.h> |
4 |
class Student |
5 |
{ |
6 |
public: |
7 |
int m_nStudentNO; //学号 |
8 |
char* m_pName; //姓名 |
9 |
Student(int no, char* pName) |
10 |
{ |
11 |
m_nStudentNO = no; |
12 |
m_pName = new char[20]; //分配内存空间 |
13 |
strcpy(m_pName, pName); |
14 |
} |
15 |
~Student( ) |
16 |
{ |
17 |
delete m_pName; //释放内存空间 |
18 |
} |
19 |
void print( ) |
20 |
{ |
21 |
cout<<"学号:"<<m_nStudentNO<<endl; |
22 |
cout<<"姓名:"<<m_pName<<endl; |
23 |
} |
24 |
}; |
25 |
void main( ) |
26 |
{ |
27 |
Student* pStudent = new Student(21, "小明"); |
28 |
pStudent->print( ); |
29 |
free(pStudent); //释放内存空间 |
30 |
} |
[编译结果]
编译通过,运行结果也正常。
[问题分析]
虽然代码通过编译,运行结果也正常。但倒数第2行代码使用free来为用new创建的动态对象pStudent释放内存,会导致析构函数不能运行。而Student类的析构函数里需要执行delete m_pName;这将导致m_pName指向的内存空间不能被释放,导致内存泄漏。
[正确代码]
代码修改如下:
29 |
delete pStudent; //释放内存空间 |
[运行结果]
学号:21
姓名:小明
[例13.2] 创建一个链表。
[错误代码]
1 |
#include "stdlib.h" |
2 |
#include "stdio.h" |
3 |
#define COUNT 5 |
4 |
class node //链表节点类 |
5 |
{ |
6 |
public: |
7 |
int data; |
8 |
node *next; |
9 |
}; |
10 |
void main( ) |
11 |
{ |
12 |
node* ptr = NULL; |
13 |
node* head = NULL; //链表首节点指针 |
14 |
node* prev = NULL; //链表中前一个节点指针 |
15 |
for(int i=0;i<COUNT;i++) //初始化链表节点 |
16 |
{ |
17 |
ptr = new node; |
18 |
if(!head) head = ptr; |
19 |
printf("请输入一个整数:"); |
20 |
scanf("%d",&ptr->data); |
21 |
if(prev) prev->next = ptr; |
22 |
prev = ptr; |
23 |
} |
24 |
ptr->next = NULL; |
25 |
|
26 |
ptr = head; |
27 |
while(ptr!=NULL) //输出链表 |
28 |
{ |
29 |
printf("%d\t",ptr->data); |
30 |
ptr=ptr->next; |
31 |
} |
32 |
printf("\n"); |
33 |
} |
[编译结果]
编译通过,运行结果也正常。
[问题分析]
虽然代码通过编译,运行结果也正常。但第17行代码ptr
= new node使用new来动态分配内存并创建链表节点,而程序中没有用delete来释放这些链表节点,导致内存泄漏。
[正确代码]
把第30行及之后的代码改为:
30 |
prev = ptr; |
31 |
ptr=ptr->next; |
32 |
delete prev; |
31 |
} |
32 |
printf("\n"); |
33 |
} |
[运行结果]
请输入一个整数:8
请输入一个整数:4
请输入一个整数:2
请输入一个整数:1
请输入一个整数:0
8 4 2 1 0
[例13.3] 输入一个整数N,依次输出1,2,……,N的平方。
[错误代码]
1 |
#include "stdio.h" |
2 |
void main( ) |
3 |
{ |
4 |
int count; |
5 |
printf("请输入一个整数:"); |
6 |
scanf("%d",&count); |
7 |
|
8 |
int* ptr = NULL; |
9 |
if(count>0) |
10 |
{ |
11 |
ptr = new int[count]; |
12 |
for(int i=1;i<=count;i++) |
13 |
{ |
14 |
*ptr = i*i; //计算i的平方 |
15 |
printf("%d\t",*ptr); |
16 |
ptr++; |
17 |
} |
18 |
printf("\n"); |
19 |
delete ptr; |
20 |
} |
21 |
} |
[编译结果]
编译通过,但运行时报错:
图13.1一个指针被delete时,没有指向最初的地址
[问题分析]
如果一个指针通过 +、-
等操作而改变了指向,那么在释放之前,应确保其回到原来的指向。对于本例,在循环中,每次循环时ptr++,循环结束后,ptr已经不是指向原来的内存空间。在delete时,没有使它回到原来的指向,导致出错。解决办法是在最初时“备份”一份。在释放时,直接释放该指针即可。如:
int* pbak = ptr;
……
delete pbak;
[正确代码]
把第12行及之后的代码改为:
12 |
int*
pbak = ptr; |
13 |
for(int i=1;i<=count;i++) |
14 |
{ |
15 |
*ptr = i*i;
//计算i的平方 |
16 |
printf("%d\t",*ptr); |
17 |
ptr++; |
18 |
} |
19 |
printf("\n"); |
20 |
delete
pbak; |
21 |
} |
22 |
} |
[运行结果]
请输入一个整数:6
1 4 9 16 25 36
[例13.4] 重复释放已释放的空间例子。
[错误代码]
1 |
#include "iostream.h" |
2 |
void main( ) |
3 |
{ |
4 |
int* p = new int(5); |
5 |
cout<<*p<<endl; |
6 |
delete p; |
7 |
delete p; |
8 |
} |
[编译结果]
编译通过,但运行时报错:
图13.2 重复释放已释放的空间
[问题分析]
重复释放已释放的空间会导致程序终止。本例中出现了两次delete p。
[正确代码]
只需要删除多余的代码:
7 |
//delete p; //删除这句代码 |
[运行结果]
5
[例13.5] 两个指针指向同一个整数所在的内存空间。
[错误代码]
1 |
#include "iostream.h" |
2 |
void main( ) |
3 |
{ |
4 |
int* p1 = new int(5); |
5 |
int* p2 = p1; |
6 |
cout<<*p1<<endl; |
7 |
cout<<*p2<<endl; |
8 |
delete p1; |
9 |
delete p2; |
10 |
} |
[编译结果]
编译通过,但运行时报错:
图13.3重复delete同一指向的多个指针
[问题分析]
p1和p2两个指针指向同一个整数所在的内存空间。p2所指向的内存空间,已通过delete p1而被释放,不可再delete一次。同样的问题,如果你delete了p2,则同样不可再delete p1。
[正确代码]
第8、9两行代码需要删除其中的一句:
9 |
//delete p2; //删除这句代码 |
[运行结果]
5
5
[例13.6] 一个指针指向一个整数所在的内存空间。
[错误代码]
1 |
#include "iostream.h" |
2 |
void main( ) |
3 |
{ |
4 |
int a = 100; |
5 |
int *p = &a; |
6 |
cout<<*p<<endl; |
7 |
delete p; |
8 |
} |
[编译结果]
编译通过,但运行时报错:
图13.4 delete指向某一普通变量的指针
[问题分析]
p 不是通过new
分配到的内存空间,而是直接指向固定变量a. 所以delete
p等于要强行释放a的固有空间,会导致出错。
[正确代码]
删除第7行代码:
7 |
//delete p; //删除这句代码 |
[运行结果]
100
请改正如下程序中的错误。
1) 为一个整型数组动态分配空间,依次输出每个元素所在内存单元的地址。
1 |
#include <iostream.h> |
2 |
void main( ) |
3 |
{ |
4 |
int* p = new int[5]; |
5 |
for(int i=0;i<5;i++) |
6 |
{ |
7 |
cout<<p<<endl; |
8 |
p++; |
9 |
} |
10 |
delete p; |
11 |
} |
2) 为整型数组a[n],b[n],c[n]动态分配空间,把数组a、b里对应的元素相乘,结果放到数组c对应的元素里。
1 |
#include<iostream.h> |
2 |
void main( ) |
3 |
{ |
4 |
int n; |
5 |
cin>>n; |
6 |
if(n<=0) return; |
7 |
|
8 |
int *a = new int[n]; |
9 |
if(a==0) |
10 |
{ |
11 |
cout<<"内存分配失败!"<<endl; |
12 |
return; |
13 |
} |
14 |
int *b = new int[n]; |
15 |
if(b==0) |
16 |
{ |
17 |
cout<<"内存分配失败!"<<endl; |
18 |
return; |
19 |
} |
20 |
int *c = new int[n]; |
21 |
if(c==0) |
22 |
{ |
23 |
cout<<"内存分配失败!"<<endl; |
24 |
return; |
25 |
} |
26 |
for(int i=0;i<n;i++) |
27 |
{ |
28 |
a[i] = i; |
29 |
b[i] = n-i; |
30 |
} |
31 |
for(i=0;i<n;i++) |
32 |
{ |
33 |
c[i]=a[i]*b[i]; |
34 |
cout<<c[i]<<"\t"; |
35 |
} |
36 |
cout<<endl; |
37 |
} |
3) 写一个CBuffer类,按照要求设定缓冲区大小。可以向缓冲区内写入新的字符串。
1 |
#include<stdlib.h> |
2 |
#include<iostream.h> |
3 |
#include<string.h> |
4 |
class CBuffer |
5 |
{ |
6 |
public: |
7 |
CBuffer(int size) |
8 |
{ |
9 |
buffer = new char[size]; |
10 |
buffer[0] = '\0'; |
11 |
} |
12 |
~CBuffer( ) |
13 |
{ |
14 |
delete buffer; |
15 |
} |
16 |
void Append(char* str) |
17 |
{ |
18 |
strcat(buffer,str); |
19 |
} |
20 |
void print( ) |
21 |
{ |
22 |
cout<<buffer<<endl; |
23 |
} |
24 |
private: |
25 |
char* buffer; |
26 |
}; |
27 |
void main( ) |
28 |
{ |
29 |
CBuffer* buf = new CBuffer(1024); |
30 |
buf->Append("Hello,"); |
31 |
buf->Append("world!"); |
32 |
buf->print( ); |
33 |
free(buf); |
34 |
} |
引用是C++中的一个特殊的数据类型描述,它使两个以上的变量名指向同一地址,对其中任一个变量的操作实际上都是对该地址单元进行的。被声明为引用类型的变量名其实是实际变量名的别名。引用运算符为&,其形式为:
数据类型 &引用变量名
= 变量名;
或 数据类型& 引用变量名
= 变量名;
或 数据类型 & 引用变量名
= 变量名;
例如有一个整型变量num:
int num;
可以定义它的引用ref为:
int &ref = num;
或
int& ref = num;
或
或 int & ref = num;
使用引用时需注意:
(1) 在一行上声明多个引用型变量(函数)名时,要在每个变量(函数)名前都冠以“&”符号。如:
int &ref1 = num, &ref2 = num;
(2)引用被声明时必须进行初始化,除非是用作函数的参数或返回值。为引用提供的初始值应为变量(包括对象)。一旦引用被初始化,就不能改变引用的关系。引用不能用数据类型来初始化。
(3)不能有空引用,引用必须与合法的存储单元关联。
(4)由于引用没有地址,所以引用的引用,指向引用的指针,或引用的数组都是不合法的。
但可以说明对指针变量的引用,因为指针也是变量。
(5)可以用一个引用初始化另一个引用。例如:
int num;
int &ref1 = num;
int &ref2 = ref1;
(6)要注意区分引用运算符和地址运算符的区别。
(7)函数的参数可以是引用。
(8)函数可以返回引用,函数调用可以作为左值。
引用与指针有如下区别:
(1)引用被创建的同时必须被初始化,而指针则可以在任何时候被初始化。
(2)不能有空引用,引用必须与合法的存储单元关联,而指针则可以是NULL。
(3)一旦引用被初始化,就不能改变引用的关系,而指针则可以随时改变所指的对象。
[例14.1] 为一个整型变量建立4个引用。
[错误代码]
1 |
#include "iostream.h" |
2 |
void main( ) |
3 |
{ |
4 |
int m = 5; |
5 |
int &ref1 = m, ref2 = m; |
6 |
int &ref3; |
7 |
ref3 = m; |
8 |
int *p=NULL; |
9 |
int &ref4 = *p; |
10 |
cout<<ref1<<"\t"<<ref2<<"\t"<<ref3<<"\t"<<ref4<<endl; |
11 |
} |
[编译结果]
E:\CODE\14_1.cpp(6)
: error C2530: 'ref3' : references must be initialized
[问题分析]
(1)int
&ref1 = m, ref2 = m;
在一行上声明多个引用型变量(函数)名时,要在每个变量(函数)名前都冠以“&”符号。这里的ref2实际上是一个整型变量,而不是引用。如果想把ref2声明为引用,则需改为:int &ref1 = m, &ref2 = m;
(2)int
&ref3;
ref3 = m;
引用被声明时必须进行初始化,除非是用作函数的参数或返回值。一旦引用被初始化,就不能改变引用的关系。这里试图先声明引用,再赋值,是不允许的。
(3)int
*p=NULL;
int &ref4 = *p;
不能有空引用,引用必须与合法的存储单元关联,而指针则可以是NULL。所以前一句合法,后一句也合法,但会导致程序中止,因为这两句代码实际上产生了一个空引用。
[正确代码]
代码修改为:
5 |
int &ref1 = m, &ref2
= m; |
6 |
int &ref3=m; |
7 |
//ref3
= m; //删除这句代码 |
8 |
int *p=&m; |
[运行结果]
5 5 5 5
[例14.2] 写一个函数,输入指定数目的浮点数,统计出最大值、最小值、平均值。
[错误代码]
1 |
#include "iostream.h" |
2 |
void Statistic(int count, float &max, float &min, float &avg) |
3 |
{ |
4 |
for(int i=0;i<count;i++) |
5 |
{ |
6 |
float num; |
7 |
cout<<"请输入一个数:"; |
8 |
cin>>num; |
9 |
if(!i) |
10 |
{ |
11 |
max = num; |
12 |
min = num; |
13 |
avg = num; |
14 |
} |
15 |
else |
16 |
{ |
17 |
if(num>max) max=num; |
18 |
if(num<min) min=num; |
19 |
avg+=num; |
20 |
} |
21 |
} |
22 |
avg /= count; |
23 |
} |
24 |
|
25 |
void main( ) |
26 |
{ |
27 |
float maxValue,minValue,avgValue; |
28 |
|
29 |
Statistic(5,&maxValue,&minValue,&avgValue); |
30 |
|
31 |
cout<<"---统计结果---"<<endl; |
32 |
cout<<"最大值:"<<maxValue<<endl; |
33 |
cout<<"最小值:"<<minValue<<endl; |
34 |
cout<<"平均值:"<<avgValue<<endl; |
35 |
} |
[编译结果]
E:\CODE\14_2.cpp(29)
: error C2664: 'Statistic' : cannot convert parameter 2 from 'float *' to
'float &'
A
reference that is not to 'const' cannot be bound to a non-lvalue
[问题分析]
引用的一个重要作用就是作为函数的参数。C语言中函数参数一般是传递值,如果有大块数据作为参数传递的时候,往往采用指针,因为这样可以避免将整块数据全部压栈,可以提高程序的效率。C++中增加了一种同样有效率的选择(在某些特殊情况下又是必须的选择),就是引用。
本例中,函数Statistic的参数max、min、avg均为引用。为在程序中调用该函数,则相应的主调函数的调用点处,直接以变量作为实参进行调用即可,而不需要实参变量有任何的特殊要求。即:
Statistic(5,maxValue,minValue,avgValue);
而不是
Statistic(5,&maxValue,&minValue,&avgValue);
[正确代码]
代码修改为:
29 |
Statistic(5,maxValue,minValue,avgValue); |
[运行结果]
请输入一个数:1
请输入一个数:2
请输入一个数:3
请输入一个数:4
请输入一个数:5.5
---统计结果---
最大值:5.5
最小值:1
平均值:3.1
[例14.3] 写一个函数分别统计男、女生的数量。要求引用作为返回值,函数调用作为左值。
[错误代码]
1 |
#include "iostream.h" |
2 |
int &Statistic(int &male, int &female) |
3 |
{ |
4 |
int num; |
5 |
cin>>num; |
6 |
if(num) |
7 |
return ♂ |
8 |
else |
9 |
return ♀ |
10 |
} |
11 |
void main( ) |
12 |
{ |
13 |
int maleCount=0; |
14 |
int femaleCount=0; |
15 |
|
16 |
cout<<"请输入:0-女,其他-男"<<endl; |
17 |
for(int i=0;i<5;i++) |
18 |
Statistic(maleCount,femaleCount)++; |
19 |
|
20 |
cout<<"---统计结果---"<<endl; |
21 |
cout<<"男生数量:"<<maleCount<<endl; |
22 |
cout<<"女生数量:"<<femaleCount<<endl; |
23 |
} |
[编译结果]
E:\CODE\14_3.cpp(7)
: error C2440: 'return' : cannot convert from 'int *' to 'int &'
A
reference that is not to 'const' cannot be bound to a non-lvalue
E:\CODE\14_3.cpp(9)
: error C2440: 'return' : cannot convert from 'int *' to 'int &'
A
reference that is not to 'const' cannot be bound to a non-lvalue
Error
executing cl.exe.
[问题分析]
函数Statistic返回类型是引用,所以应该返回male和female这两个引用值,而不是&male和&female。
[正确代码]
代码修改如下:
7 |
return male; |
以及:
9 |
return female; |
[运行结果]
请输入:0-女,其他-男
1
1
0
0
1
---统计结果---
男生数量:3
女生数量:2
请改正如下程序中的错误。
1) 写一个函数,传入圆的半径,返回周长和面积并输出。
1 |
#include<iostream.h> |
2 |
const float PI=3.1415926; |
3 |
void calc(float r,float &s,float &l) |
4 |
{ |
5 |
s = PI*r*r; //面积 |
6 |
l= 2*PI*r; //周长 |
7 |
} |
8 |
void main( ) |
9 |
{ |
10 |
float r,s,l; |
11 |
cout<<"r="; |
12 |
cin>>r; |
13 |
calc(r, &s, &l); |
14 |
cout<<"s="<<s<<endl; |
15 |
cout<<"l="<<l<<endl; |
16 |
} |
2) 写一个函数以统计给定字符串中大写字母、小写字母、数字以及其他字符的个数。
1 |
#include "iostream.h" |
2 |
int &Statistic(char ch, int &upperCase, int &lowerCase, int &number, int &other) |
3 |
{ |
4 |
if(ch>='A' && ch<='Z') //大写字母 |
5 |
return &upperCase; |
6 |
else if(ch>='a' && ch<='z') //小写字母 |
7 |
return &lowerCase; |
8 |
else if(ch>='0' && ch<='9') //数字 |
9 |
return &number; |
10 |
else //其他字符 |
11 |
return &other; |
12 |
|
13 |
} |
14 |
void main( ) |
15 |
{ |
16 |
char str[256] = "aBcDg8K3!"; |
17 |
int upperCase=0; |
18 |
int lowerCase=0; |
19 |
int number=0; |
20 |
int other=0; |
21 |
|
22 |
char* p = str; |
23 |
while(*p != '\0') |
24 |
{ |
25 |
Statistic(*p, upperCase, lowerCase, number, other)++; |
26 |
p++; |
27 |
} |
28 |
|
29 |
cout<<"大写字母个数:"<<upperCase<<endl; |
30 |
cout<<"小写字母个数:"<<lowerCase<<endl; |
31 |
cout<<"数字个数:"<<number<<endl; |
32 |
cout<<"其他字符个数:"<<other<<endl; |
33 |
} |
C++可以用const定义常量,也可以用#define定义常量,但是前者比后者有更多的优点:
(1)const常量有数据类型,而宏常量没有数据类型。编译器可以对const进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换中可能会产生意料不到的错误。
(2)有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。在C++程序中建议只使用const常量而不使用宏常量,即用const常量完全取代宏常量。
(3)编译器处理方式不同。define宏是在预处理阶段展开。const常量是编译运行阶段使用。
(4)存储方式不同。define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。 const常量会在内存中分配(可以是堆中也可以是栈中)。
用#define定义常量和用const定义常量时的格式分别如下:
#define PI 3.1415926
const float PI=3.1415926;
使用const修饰符时需要注意以下几点:
(1)使用const修饰符定义常量时,必须初始化;
(2)常量一旦被定义,在程序中任何地方都不能再更改。
(3)const定义的常量可以有自己的数据类型,这样C++编译程序可以进行更加严格的类型检查。如果用const定义的是一个整型常量,int可以省略。
(4)函数参数可以用const说明,用于保证实参在该函数内部不被改动,大多数C++编译器能对具有const参数的函数进行更好的代码优化。
(5)函数返回值为const只用在函数返回为引用的情况。 函数返回值引用常量表示不能将函数调用表达式作为左值使用。
(6)在类中,可以在类的成员函数定义后面加上const,表示这个函数是一个“只读函数”,函数不能改变类对象的状态,不能改变对象的成员变量的值。const成员函数也不能在函数中调用其他非const 的函数。
const与指针一起使用时有三种情况:
(1)指向常量的指针,如const char* pc="abcd";它声明了指向常量的指针变量pc,不允许改变指针所指的常量,但是由于pc是一个指向常量的普通指针变量,因此可以改变pc的值。
(2)常指针,如char* const pc="abcd";它声明了一个名为pc的指针变量,该指针是指向字符型数据的常指针,用“abcd”的地址初始化该常指针。该指针不能移动,但是它所指的数据可以改变。
(3)指向常量的常指针,如const char* const pc="abcd";它声明了一个名为pc的指针变量,它是一个指向字符型常量的常指针,用“abcd”的地址初始化该指针。整个指针本身不能改变,它所指向的值也不能改变。
[例15.1] 计算圆的周长和面积。
[错误代码]
1 |
#include <iostream> |
2 |
using namespace std; |
3 |
const PI 3.14 |
4 |
void main( ) |
5 |
{ |
6 |
int r = 10; //半径 |
7 |
double p,s; |
8 |
p = 2*r*PI; //周长 |
9 |
s = r*r*PI; //面积 |
10 |
cout<<"半径:"<<r<<endl; |
11 |
cout<<"周长:"<<p<<endl; |
12 |
cout<<"面积:"<<s<<endl; |
13 |
} |
[编译结果]
E:\CODE\15_1.cpp(3)
: error C2143: syntax error : missing ';' before 'constant'
E:\CODE\15_1.cpp(3)
: error C2734: 'PI' : const object must be initialized if not extern
E:\CODE\15_1.cpp(3)
: fatal error C1004: unexpected end of file found
[问题分析]
这里把用const定义常量和用#define定义常量弄混了。二者格式分别如下:
#define PI
3.14
const double
PI=3.14;
const定义的常量可以有自己的数据类型。如果用const定义的是一个整型常量,int可以省略。本例中除了缺少等号和分号以外,“double”是不能省略的。
[正确代码]
代码修改如下:
3 |
const double PI=3.14; |
[运行结果]
半径:10
周长:62.8
面积:314
[例15.2] 建立一个整型数组,按照从小到大排序。
[错误代码]
1 |
#include <iostream.h> |
2 |
const COUNT = 5; |
3 |
void sort(const int* data) |
4 |
{ |
5 |
for(int i=0;i<COUNT;i++) |
6 |
{ |
7 |
for(int j=i+1;j<COUNT;j++) |
8 |
{ |
9 |
if(data[i]>data[j]) //前者大则交换数据 |
10 |
{ |
11 |
int tmp = data[i]; |
12 |
data[i]=data[j]; |
13 |
data[j]=tmp; |
14 |
} |
15 |
} |
16 |
} |
17 |
} |
18 |
void main( ) |
19 |
{ |
20 |
int d[5]; |
21 |
for(int i=0;i<COUNT;i++) |
22 |
{ |
23 |
cout<<"请输入一个整数:"; |
24 |
cin>>d[i]; |
25 |
} |
26 |
sort(d); |
27 |
for(int j=0;j<COUNT;j++) |
28 |
cout<<d[j]<<"\t"; |
29 |
cout<<endl; |
30 |
} |
[编译结果]
E:\CODE\15_2.cpp(12)
: error C2166: l-value specifies const object
E:\CODE\15_2.cpp(13)
: error C2166: l-value specifies const object
[问题分析]
函数sort的参数为const
int* data,即指针data为指向常量的指针,不允许改变指针所指的常量。但sort函数要对这些数据按照从小到大排序,而且排好序后仍保存在原来的数组里。因此,需要把const int* data改为int*
const data,或者int* data。
[正确代码]
3 |
void sort(int* const data) |
[运行结果]
请输入一个整数:18
请输入一个整数:3
请输入一个整数:9
请输入一个整数:1
请输入一个整数:12
1 3 9 12 18
[例15.3] 定义一个Employee类,可以对数据成员ID进行访问。
[错误代码]
1 |
#include <iostream.h> |
2 |
class Employee |
3 |
{ |
4 |
private: |
5 |
int ID; |
6 |
public: |
7 |
int getID( ) const |
8 |
{ |
9 |
return ID; |
10 |
} |
11 |
void setID(int id) const |
12 |
{ |
13 |
ID = id; |
14 |
} |
15 |
void print( ) const |
16 |
{ |
17 |
cout<<"ID="<<ID<<endl; |
18 |
} |
19 |
}; |
20 |
void main( ) |
21 |
{ |
22 |
Employee employee; |
23 |
employee.setID(5); |
24 |
employee.print( ); |
25 |
} |
[编译结果]
E:\CODE\15_3.cpp(13)
: error C2166: l-value specifies const object
[问题分析]
在类中,可以在类的成员函数定义后面加上const,表示这个成员函数是一个“只读函数”,函数不能改变类对象的状态,也不能改变对象的成员变量的值。const成员函数也不能在函数中调用其他非const 的函数。
在本例中,Employee类的成员函数getID( )和print( )都不需要改变数据成员的值,所以把它们定义为const函数是可以的;但成员函数setID(int id)需要修改数据成员ID的值,所以不能把它们定义为const函数。
[正确代码]
11 |
void setID(int id) //删除此处的const |
[运行结果]
ID=5
请改正如下程序中的错误。
1) 写一个函数,把“光年”转换为“公里”。
1 |
#include <iostream.h> |
2 |
const double LIGHTSPEED 300000 //光速 |
3 |
double Convert(const double *ly) |
4 |
{ |
5 |
*ly *= LIGHTSPEED*365*24*60*60; //把“光年”转换为“公里” |
6 |
return *ly; |
7 |
} |
8 |
void main( ) |
9 |
{ |
10 |
double nLightYear = 1; |
11 |
cout<<"一光年相当于"<<Convert(&nLightYear)<<"公里。"<<endl;; |
12 |
} |
2) 定义一个“收音机”类,可以通过set成员函数对数据成员进行初始化。
1 |
#include <string> |
2 |
#include <iostream> |
3 |
using namespace std; |
4 |
class Radio //“收音机”类 |
5 |
{ |
6 |
public: |
7 |
string brand; //品牌 |
8 |
string type; //型号 |
9 |
void set(string b, string t) const |
10 |
{ |
11 |
brand = b; |
12 |
type = t; |
13 |
} |
14 |
void Display( ) const //输出品牌和型号 |
15 |
{ |
16 |
cout<<brand<<endl; |
17 |
cout<<type<<endl; |
18 |
} |
19 |
}; |
20 |
void main( ) |
21 |
{ |
22 |
Radio r; |
23 |
r.set("Panasonic","ES4036"); |
24 |
r.Display( ); |
25 |
} |
除了实现计算功能外,文本处理也是编程过程中一个非常重要的方面。在C语言中使用字符数组和字符指针实现字符串,但是在C++中提供了一种既方便又好用的string类型。
[例16.1] 使用string类型进行字符串操作。
[错误代码]
1 |
#include <iostream.h> |
2 |
#include <string.h> |
3 |
void main( ) |
4 |
{ |
5 |
string s1 = "Hello"; |
6 |
string s2 = "world"; |
7 |
string s3 = s1 + ", " + s2 + "!\n"; |
8 |
cout<<s3; |
9 |
} |
[编译结果]
E:\CODE\16_1.cpp(5)
: error C2065: 'string' : undeclared identifier
E:\CODE\16_1.cpp(5)
: error C2146: syntax error : missing ';' before identifier 's1'
E:\CODE\16_1.cpp(5)
: error C2065: 's1' : undeclared identifier
E:\CODE\16_1.cpp(5)
: error C2440: '=' : cannot convert from 'char [6]' to 'int'
This conversion requires a
reinterpret_cast, a C-style cast or function-style cast
E:\CODE\16_1.cpp(6)
: error C2146: syntax error : missing ';' before identifier 's2'
E:\CODE\16_1.cpp(6)
: error C2065: 's2' : undeclared identifier
E:\CODE\16_1.cpp(6)
: error C2440: '=' : cannot convert from 'char [6]' to 'int'
This conversion requires a
reinterpret_cast, a C-style cast or function-style cast
E:\CODE\16_1.cpp(7)
: error C2146: syntax error : missing ';' before identifier 's3'
E:\CODE\16_1.cpp(7)
: error C2065: 's3' : undeclared identifier
E:\CODE\16_1.cpp(7)
: error C2110: cannot add two pointers
[问题分析]
string类型是C++提供的,被包含在std这个namespace中。标准C++库的头文件是没有.h的,全部放在std的名字空间里。所以使用string的时候必须写using amespace std; 或者std::string。
[正确代码]
把main函数之前的代码改为:
1 |
#include <iostream> |
2 |
#include <string> |
3 |
using namespace std; |
[运行结果]
Hello, world!
请改正如下程序中的错误。
1) 从控制台读入姓名,并输出该姓名信息。
1 |
#include <iostream.h> |
2 |
#include <string.h> |
3 |
void main( ) |
4 |
{ |
5 |
string sName; |
6 |
string str; |
7 |
|
8 |
cout<<"请输入姓名:"; |
9 |
cin>>sName; |
10 |
str = "姓名:" + sName; |
11 |
cout<<str<<endl; |
12 |
} |
1.内联函数
调用函数时首先必须保存现场并记忆执行的地址,函数执行完毕后要恢复现场,并按原来保存地址继续执行。因此,函数调用要有一定的时间和空间方面的开销,频繁调用会降低执行效率。
内联函数具有一般函数的特性,它与一般函数所不同之处只在于函数调用的处理。一般函数进行调用时,要将程序执行权转到被调用函数中,执行完毕后再返回到调用它的函数中;而内联函数在调用时,是将调用表达式用内联函数体来替换。
一般情况下,对内联函数做如下的限制:
(1) 不能含有递归;
(2) 不能包含静态数据;
(3) 不能包含循环;
(4) 不能包含switch和goto语句;
(5) 不能包含数组。
若一个内联函数定义不满足以上限制,则编译系统把它当作普通函数对待。
2.函数的缺省参数
在函数说明或函数定义中可以为形参指定一个缺省值。如果函数有多个缺省参数,则缺省参数必须是从右向左定义,即在一个缺省参数的右边不能有未指定缺省值的参数。如果在函数原型的声明中设置了函数参数的缺省值,则不可再在函数定义的头部重复设置,否则会导致编译错误。
另外,如果某个函数参数有缺省值,必须保证其参数缺省后调用形式不与其它函数混淆。例如:
int foo(int a, float b);
void foo(int a, float b, int c=0);
则函数调用语句:
foo(10, 2.0);
具有二义性,既可以调用第一个函数,也可以调用第二个函数,编译器不能根据参数的形式确定到底调用哪一个。
3.函数重载
C++编译系统允许为两个或两个以上的函数取相同的函数名,但是形参的个数或者形参的类型不应该完全相同,编译系统会根据实参和形参的类型及个数的最佳匹配,自动确定调用哪一个函数,这就是函数重载。重载函数时要注意,不可以定义两个具有相同名称、相同参数类型和相同参数个数,只是函数返回值不同的函数。
4.函数模板
C++语言中可以使用模板来避免在程序中多次书写相同的代码。所谓模板是一种使用无类型参数来产生一系列函数或类的机制,是C++的一个重要特征。通过模板可以产生类或函数的集合,使它们操作不同的数据类型,从而避免为每一种数据类型产生一个单独的类或函数。模板分为函数模板和类模板。
[例17.1] 定义一个内联函数,把英寸转换为厘米。
[错误代码]
1 |
#include <iostream.h> |
2 |
double InchToCM(double inch); |
3 |
void main( ) |
4 |
{ |
5 |
float Inch = 2; |
6 |
cout<<"2英寸="<<InchToCM(Inch)<<"厘米"<<endl; |
7 |
} |
8 |
inline double InchToCM(double inch) |
9 |
{ |
10 |
return 2.54*inch; |
11 |
} |
[编译结果]
编译通过。
[问题分析]
如果在内联函数的声明处省略inline关键字,即使在函数定义时加上inline关键字,编译程序也不认为那是内联函数,而当作普通函数。实际上只要在声明时加上inline关键字即可,函数定义时可以省略。
[正确代码]
把代码改为:
2 |
inline double InchToCM(double inch); |
[运行结果]
2英寸=5.08厘米
[例17.2] 写一个函数,格式化输出一个日期,缺省为
[错误代码]
1 |
#include <string> |
2 |
#include <iostream> |
3 |
using namespace std; |
4 |
string FormatDate(int year=2011,int month,int day); |
5 |
|
6 |
void main( ) |
7 |
{ |
8 |
cout<<FormatDate(8,1)<<endl; |
9 |
} |
10 |
|
11 |
string FormatDate(int year=2011,int month,int day) |
12 |
{ |
13 |
string str; |
14 |
char date[50]; |
15 |
sprintf(date, "%d年%d月%d日",year,month, day); |
16 |
str = date; |
17 |
return str; |
18 |
} |
[编译结果]
E:\CODE\17_2.cpp(4)
: error C2548: 'FormatDate' : missing default parameter for parameter 2
E:\CODE\17_2.cpp(4)
: error C2548: 'FormatDate' : missing default parameter for parameter 3
E:\CODE\17_2.cpp(8)
: error C2660: 'FormatDate' : function does not take 2 parameters
E:\CODE\17_2.cpp(12)
: error C2548: 'FormatDate' : missing default parameter for parameter 2
E:\CODE\17_2.cpp(12)
: error C2548: 'FormatDate' : missing default parameter for parameter 3
[问题分析]
本例中犯了两个错误:
(1)如果函数有多个缺省参数,则缺省参数必须是从右向左定义,即在一个缺省参数的右边不能有未指定缺省值的参数。函数FormatDate的第一个参数year设定了缺省值2011,那么后面的两个参数就必须有缺省值,所以编译器会认为缺少第2、3个缺省参数。在main主函数中FormatDate(8,1)也是不符合要求的。
(2)如果在函数原型的声明中设置了函数参数的缺省值,则不可再在函数定义的头部重复设置,否则会导致编译错误。
[正确代码]
代码修改为:
4 |
string FormatDate(int year=2011,int month=8,int day=1); |
以及:
11 |
string FormatDate(int year/*=2011*/,int month/*=8*/,int day/*=1*/) |
[运行结果]
[例17.3] 写两个函数分别计算两个复数的和,一个返回复数结构,另一个返回字符串。
[错误代码]
1 |
#include <string> |
2 |
#include <iostream> |
3 |
using namespace std; |
4 |
struct complex //复数结构 |
5 |
{ |
6 |
double real; //复数的实部 |
7 |
double imag; //复数的虚部 |
8 |
}; |
9 |
complex add(complex c1,complex c2) |
10 |
{ |
11 |
complex c; |
12 |
c.real=c1.real+c2.real; |
13 |
c.imag=c1.imag+c2.imag; |
14 |
return c; |
15 |
} |
16 |
string add(complex c1,complex c2) |
17 |
{ |
18 |
complex c = add(c1,c2); |
19 |
char tmp[50]; |
20 |
sprintf(tmp, "% |
21 |
string str = tmp; |
22 |
return str; |
23 |
} |
24 |
void main(void) |
25 |
{ |
26 |
complex c1,c2; |
27 |
c1.real = 3; |
28 |
c1.imag = 4; |
29 |
c2.real = 5; |
30 |
c2.imag = 6; |
31 |
string str = add(c1,c2); |
32 |
cout<<str<<endl; |
33 |
} |
[编译结果]
E:\CODE\17_3.cpp(17)
: error C2556: 'class std::basic_string<char,struct
std::char_traits<char>,class std::allocator<char> > __cdecl
add(struct complex,struct complex)' : overloaded function differs only by
return type from 'struct complex __cdecl add(struct complex,struct complex)'
E:\CODE\17_3.cpp(9)
: see declaration of 'add'
E:\CODE\17_3.cpp(17)
: error C2371: 'add' : redefinition; different basic types
E:\CODE\17_3.cpp(9)
: see declaration of 'add'
E:\CODE\17_3.cpp(18)
: error C2440: 'initializing' : cannot convert from '' to 'struct complex'No
constructor could take the source type, or constructor overload resolution was
ambiguous
E:\CODE\17_3.cpp(31)
: error C2440: 'initializing' : cannot convert from '' to 'class std::basic_string<char,struct
std::char_traits<char>,class std::allocator <char> >'No
constructor could take the source type, or constructor overload resolution was
ambiguous
[问题分析]
重载函数时形参的个数或者形参的类型不应该完全相同,编译系统会根据实参和形参的类型及个数的最佳匹配,自动确定调用哪一个函数。但是要注意,不可以定义两个具有相同名称、相同参数类型和相同参数个数,而只是函数返回值不同的函数。本例中add函数的两种形式,参数都是两个complex,而返回类型不同,是错误的。把其中的一个改名字即可。
[正确代码]
代码修改为:
16 |
string add2(complex c1,complex c2) |
以及:
31 |
string str = add2(c1,c2); |
[运行结果]
8.00+10.00i
[例17.4] 编程计算两个或三个给定整数的和。
[错误代码]
1 |
#include <iostream> |
2 |
using namespace std; |
3 |
int add(int a,int b) |
4 |
{ |
5 |
return a+b; |
6 |
} |
7 |
int add(int a,int b,int c=0) |
8 |
{ |
9 |
return a+b+c; |
10 |
} |
11 |
void main(void) |
12 |
{ |
13 |
cout<<"4+5="<<add(4,5)<<endl; |
14 |
} |
[编译结果]
E:\CODE\17_4.cpp(13)
: error C2668: 'add' : ambiguous call to overloaded function
[问题分析]
如果某个函数参数有缺省值,必须保证其参数缺省后调用形式不与其它函数混淆。本例中给出了add函数的两种形式:
int add(int a,int b)
int add(int a,int b,int c=0)
则函数调用语句:
add(4,5);
具有二义性,既可以调用第一个函数,也可以调用第二个函数,编译器不能根据参数的形式确定到底调用哪一个。实际上前一种形式完全可以去掉,因为后者已经实现了计算两个给定整数之和的功能。
[正确代码]
1 |
#include
<iostream> |
2 |
using
namespace std; |
3 |
int
add(int a,int b,int c=0) |
4 |
{ |
5 |
return a+b+c; |
6 |
} |
7 |
void
main(void) |
8 |
{ |
9 |
cout<<"4+5="<<add(4,5)<<endl; |
10 |
} |
[运行结果]
4+5=9
请改正如下程序中的错误。
1) 写一个函数,格式化输出时间。
1 |
#include <string> |
2 |
#include <iostream> |
3 |
using namespace std; |
4 |
string FormatTime(int hour=12,int minute,int second=0); |
5 |
void main( ) |
6 |
{ |
7 |
cout<<FormatTime( )<<endl; |
8 |
} |
9 |
string FormatTime(int hour=12,int minute,int second=0) |
10 |
{ |
11 |
string str; |
12 |
char Time[50]; |
13 |
sprintf(Time, "%.2d:%.2d:%.2d", hour, minute, second); |
14 |
str = Time; |
15 |
return str; |
16 |
} |
2) 格式化输出日期和时间。
1 |
#include <string> |
2 |
#include <iostream> |
3 |
using namespace std; |
4 |
char* FormatDateTime(int year,int month,int day) |
5 |
{ |
6 |
char date[50]; |
7 |
sprintf(date, "%d年%d月%d日",year,month, day); |
8 |
return date; |
9 |
} |
10 |
string FormatDateTime(int year,int month,int day) |
11 |
{ |
12 |
string str; |
13 |
char date[50]; |
14 |
sprintf(date, "%d年%d月%d日",year,month, day); |
15 |
str = date; |
16 |
return str; |
17 |
} |
18 |
string FormatDateTime(int year,int month,int day, int hour=12,int minute=0,int second=0) |
19 |
{ |
20 |
string str; |
21 |
char Time[50]; |
22 |
sprintf(Time, "%d年%d月%d日 %.2d:%.2d:%.2d", year, month, day, \ |
23 |
hour, minute, second); |
24 |
str = Time; |
25 |
return str; |
26 |
} |
27 |
void main( ) |
28 |
{ |
29 |
cout<<FormatDateTime(2011,10,1)<<endl; |
30 |
cout<<FormatDateTime(2011,10,1,9,30,30)<<endl; |
31 |
} |
本部分主要包括类与对象、友元、继承机制、多态和虚函数等内容。
类是现实世界或思维世界中的实体在计算机中的反映,它将数据以及这些数据有关的操作封装在一起。类是对象的抽象,而对象是类的具体实例。类是抽象的,不占用内存,而对象是具体的,占用存储空间。
1. 类的定义和对象的声明
类的定义形式如下:
class 类名
{
private:
[<私有成员说明表>]
public:
[<公有成员说明表>]
protected:
[<保护成员说明表>]
};
其中,class是定义类的关键字。类名是一个有效的标志符。大括号括起来的部分是类说明部分,它声明了类的所有成员(包括数据成员和成员函数),这些成员从访问权限上分成三类,即私有(private)、公有(public)和保护(protected),其中默认权限为private。private部分说明的成员,在类之外是不能访问的,只有类中的成员函数才能访问私有的数据成员和成员函数。类的public部分说明的成员,可被程序中的任何函数或语句访问。public成员函数用来提供一个与外界的接口。类的protected部分说明的成员,不能在类之外访问,只有类的成员函数及其子类(派生类)才可以访问protected的成员。
声明对象的过程叫做类的实例化。声明类对象的一般形式为:
类名 对象名表;
在类体外,类成员的访问形式为:<对象名>.成员名,或者<指向对象的指针名>->成员名。
2. 构造函数和析构函数
(1)构造函数
构造函数是一种特殊的成员函数。在C++中,对象的初始化工作是由构造函数来完成的。构造函数具有如下特征:
构造函数名与类名相同。
没有函数返回类型说明。
在新的对象被创建时,编译系统自动调用构造函数,完成初始化工作。
如果在类中没有定义构造函数,系统将给出一个默认的无参构造函数。
构造函数可以被重载。
(2)析构函数
析构函数是一个特殊的成员函数,具有如下特征:
析构函数名为:~<类名>。
没有函数返回类型说明。
析构函数无参数。
不能被重载。
如果在类中没有给出析构函数,系统会给出一个默认的析构函数。
当用delete释放动态对象时,系统自动调用析构函数完成对象撤销前的处理。
3.类的静态成员
在类的成员前加关键字static,这种成员就是类的静态成员。类的静态成员属于类,不属于任何对象。静态成员只在公共内存区域中保存一份,被该类的所有对象共享。
(1)静态数据成员
需要在类外对静态数据成员进行初始化。形式如下:
<类型> <类名>::<静态数据成员名>[=<初值>];
此时,前面不要加关键字static。如:
double Account::m_rate=0.098;
(2)静态成员函数
对类的静态成员函数的调用形式为:
<类名>::<静态成员函数调用>
4.this指针
每个类成员函数都含有一个指向被调用对象的指针,即this指针。this指针是隐含于每个非静态成员函数中的特殊指针。this指针使得类中相同的成员函数根据this指针指向对象的不同而操作不同对象的数据成员。类的静态成员函数没有this指针。这是因为静态成员函数为类的所有对象所共有,不属于某一个对象。以下两种情况时可以显式地使用this指针:
使用“return
*this”可以在类的非静态成员函数中返回类对象本身。
当参数和成员变量名相同时使用this指针区别二者。
5.局部类
在一个函数体内定义的类称为局部类。局部类只能与使用它的外围作用域中的对象和函数进行联系,因为外围作用域中的变量与该局部类的对象无关。局部类不能被外部所继承。在定义局部类时需要注意:局部类中不能说明静态成员函数,并且所有成员函数都必须定义在类体内。在实践中,局部类是很少使用的。
6.嵌套类
在一个类中定义的类称为嵌套类,定义嵌套类的类称为外围类。
定义嵌套类的目的在于隐藏类名,减少全局的标识符,从而限制用户使用该类建立对象。这样可以提高类的抽象能力,并且强调了两个类(外围类和嵌套类)之间的主从关系。
下面是一个嵌套类的例子:
class A
{
public:
class B
{
public:
…
private:
…
};
void f( );
private:
int a;
}
其中,类B是一个嵌套类,类A是外围类,类B定义在类A的类体内。
[例18.1] 定义“书”类,数据成员有书名、作者、价格等,在成员函数里输出这些信息。
[错误代码]
1 |
#include <string.h> |
2 |
#include <iostream.h> |
3 |
class Book |
4 |
{ |
5 |
public: |
6 |
char name[50]; //书名 |
7 |
char author[50]; //作者 |
8 |
float price; //价格 |
9 |
void Info( ) |
10 |
{ |
11 |
cout<<"书名:"<<name<<endl; |
12 |
cout<<"作者:"<<author<<endl; |
13 |
cout<<"价格:"<<price<<endl; |
14 |
} |
15 |
} |
16 |
void main(void) |
17 |
{ |
18 |
Book b; |
19 |
strcpy(b.name, "天龙八部"); |
20 |
strcpy(b.author, "金庸"); |
21 |
b.price = 108; |
22 |
b.Info( ); |
23 |
} |
[编译结果]
E:\CODE\18_1.cpp(16)
: error C2628: 'Book' followed by 'void' is illegal (did you forget a ';'?)
E:\CODE\18_1.cpp(23)
: warning C4508: 'main' : function should return a value; 'void' return type
assumed
[问题分析]
类的定义后面需要加分号。本例中第15行代码的“}”后面丢失了这个分号,所以编译器给出错误信息:“'Book' followed by 'void' is illegal (did you
forget a ';'?)”。
[正确代码]
把第15行代码改为
15 |
}; |
[运行结果]
书名:天龙八部
作者:金庸
价格:108
[例18.2] 定义“键盘”类,数据成员有品牌、键数、价格等,在成员函数里输出这些信息。
[错误代码]
1 |
#include <string.h> |
2 |
#include <iostream.h> |
3 |
class Keyboard |
4 |
{ |
5 |
private: |
6 |
char brand[50]; //品牌 |
7 |
int keyCount; //键数 |
8 |
float price; //价格 |
9 |
void Info( ) |
10 |
{ |
11 |
cout<<"品牌:"<<brand<<endl; |
12 |
cout<<"键数:"<<keyCount<<endl; |
13 |
cout<<"价格:"<<price<<endl; |
14 |
} |
15 |
}; |
16 |
void main(void) |
17 |
{ |
18 |
Keyboard k; |
19 |
Keyboard *p = &k; |
20 |
strcpy(p.brand, "罗技"); |
21 |
p.keyCount = 103; |
22 |
p.price = 156; |
23 |
p.Info( ); |
24 |
} |
[编译结果]
E:\CODE\18_2.cpp(20)
: error C2228: left of '.brand' must have class/struct/ union type
E:\CODE\18_2.cpp(21)
: error C2228: left of '.keyCount' must have class/struct/ union type
E:\CODE\18_2.cpp(22)
: error C2228: left of '.price' must have class/struct/ union type
E:\CODE\18_2.cpp(23)
: error C2228: left of '.Info' must have class/struct/union type
[问题分析]
一般对象的成员表示如下:
<对象名>.<成员名>
指向对象的指针的成员表示如下:
(*<对象指针名>).<成员名>
或者
<对象指针名>-><成员名>
本例中p为指向Keyboard类的实例k的指针,要访问它的成员,不能用p.<成员名>,而应该用p-><成员名>,或者(*p) .<成员名>。这样修改之后再编译,仍有错误:
E:\CODE\18_2.cpp(20)
: error C2248: 'brand' : cannot access private member declared in class
'Keyboard'
E:\CODE\18_2.cpp(6)
: see declaration of 'brand'
E:\CODE\18_2.cpp(21)
: error C2248: 'keyCount' : cannot access private member declared in class
'Keyboard'
E:\CODE\18_2.cpp(7)
: see declaration of 'keyCount'
E:\CODE\18_2.cpp(22)
: error C2248: 'price' : cannot access private member declared in class
'Keyboard'
E:\CODE\18_2.cpp(8)
: see declaration of 'price'
E:\CODE\18_2.cpp(23)
: error C2248: 'Info' : cannot access private member declared in class
'Keyboard'
E:\CODE\18_2.cpp(9)
: see declaration of 'Info'
这是因为定义Keyboard类的成员brand、keyCount、price、void Info( )之前,没有加访问权限修饰符public,而是加了private。而在main函数里,是不能访问Keyboard类的实例k的私有成员的,所以导致以上错误信息。
[正确代码]
把第5行代码改为:
5 |
public: |
把第20 ~ 23行代码改为”
20 |
strcpy(p->brand, "罗技"); |
21 |
p->keyCount = 103; |
22 |
p->price = 156; |
23 |
p->Info( ); |
[运行结果]
品牌:罗技
键数:103
价格:156
[例18.3] 定义“鼠标”类,数据成员有品牌、按键数、是否有滚轮,价格等,在成员函数里输出这些信息。
[错误代码]
1 |
#include <iostream.h> |
2 |
class Mouse |
3 |
{ |
4 |
public: |
5 |
char brand[50] = "罗技"; //品牌 |
6 |
int buttonCount = 3; //按键数 |
7 |
float price = 30; //价格 |
8 |
bool hasRoll = true; //是否有滚轮 |
9 |
void Info( ) |
10 |
{ |
11 |
cout<<"品牌:"<<brand<<endl; |
12 |
cout<<"按键数:"<<buttonCount<<endl; |
13 |
cout<<"滚轮:"<< (hasRoll ? "有" : "无") <<endl; |
14 |
cout<<"价格:"<<price<<endl; |
15 |
} |
16 |
}; |
17 |
void main(void) |
18 |
{ |
19 |
Mouse m; |
20 |
m.Info( ); |
21 |
} |
[编译结果]
E:\CODE\18_3.cpp(5)
: error C2258: illegal pure syntax, must be '= 0'
E:\CODE\18_3.cpp(5)
: error C2252: 'brand' : pure specifier can only be specified for functions
E:\CODE\18_3.cpp(6)
: error C2258: illegal pure syntax, must be '= 0'
E:\CODE\18_3.cpp(6)
: error C2252: 'buttonCount' : pure specifier can only be specified for
functions
E:\CODE\18_3.cpp(7)
: error C2258: illegal pure syntax, must be '= 0'
E:\CODE\18_3.cpp(7)
: error C2252: 'price' : pure specifier can only be specified for functions
E:\CODE\18_3.cpp(8)
: error C2258: illegal pure syntax, must be '= 0'
E:\CODE\18_3.cpp(8)
: error C2252: 'hasRoll' : pure specifier can only be specified for functions
[问题分析]
在类体中进行数据成员的初始化是不允许的。可以直接对公有属性赋值,或者在构造函数里初始化数据成员。在本例中,第5 ~ 8行代码试图进行数据成员的初始化,是错误的。
[正确代码]
1 |
#include <string.h> |
2 |
#include <iostream.h> |
3 |
class Mouse |
4 |
{ |
5 |
public: |
6 |
char brand[50];
//品牌 |
7 |
int buttonCount; //按键数 |
8 |
float price;
//价格 |
9 |
bool hasRoll;
//是否有滚轮 |
10 |
void Info( ) |
11 |
{ |
12 |
cout<<"品牌:"<<brand<<endl; |
13 |
cout<<"按键数:"<<buttonCount<<endl; |
14 |
cout<<"滚轮:"<< (hasRoll ? "有" : "无") <<endl; |
15 |
cout<<"价格:"<<price<<endl; |
16 |
} |
17 |
}; |
18 |
void main(void) |
19 |
{ |
20 |
Mouse m; |
21 |
strcpy(m.brand,
"罗技"); |
22 |
m.buttonCount = 3; |
23 |
m.price = 30; |
24 |
m.hasRoll = true; |
25 |
m.Info( ); |
26 |
} |
[运行结果]
品牌:罗技
按键数:3
滚轮:有
价格:30
[例18.4] 定义“日期”类,在成员函数里进行初始化、判断是否闰年、输出日期。
[错误代码]
1 |
#include <iostream.h> |
2 |
class Date |
3 |
{ |
4 |
private: |
5 |
int year; //年 |
6 |
int month; //月 |
7 |
int day; //日 |
8 |
public: |
9 |
void SetDate(int y, int m, int d); //设置日期值 |
10 |
bool IsLeapYear( ); //判断是否闰年 |
11 |
void Print( ); //输出 |
12 |
}; |
13 |
void SetDate(int y, int m, int d) //设置日期值 |
14 |
{ |
15 |
year = y; |
16 |
month = m; |
17 |
day = d; |
18 |
} |
19 |
bool IsLeapYear( ) //判断是否闰年 |
20 |
{ //能被4整除却不能被100整除 或 能被400整除的年份是闰年。 |
21 |
return (year%4==0 && year%100!=0) || (year%400==0); |
22 |
} |
23 |
void Print( ) //输出 |
24 |
{ |
25 |
cout<<year<<"年"<<month<<"月"<<day<<"日"<<endl; |
26 |
cout<<(IsLeapYear( ) ? "闰年" : "平年")<<endl; |
27 |
} |
28 |
void main(void) |
29 |
{ |
30 |
Date d1; |
31 |
d1.SetDate(2000,8,1); |
32 |
d1.Print( ); |
33 |
Date d2; |
34 |
d2.SetDate(2011,8,1); |
35 |
d2.Print( ); |
36 |
} |
[编译结果]
E:\CODE\18_4.cpp(15)
: error C2065: 'year' : undeclared identifier
E:\CODE\18_4.cpp(16)
: error C2065: 'month' : undeclared identifier
E:\CODE\18_4.cpp(17)
: error C2065: 'day' : undeclared identifier
[问题分析]
在本例中,成员函数:
void SetDate(int y, int m, int d);
bool IsLeapYear( );
void Print( );
的实现部分没有写在Date类的声明里,而是写在类声明的外面。这时候成员函数的实现部分应该在返回类型和函数名之间加上作用域运算符“::”,即:
void Date::SetDate(int y, int m,
int d)
bool Date::IsLeapYear( )
void Date::Print( )
正是因为这个原因,所以编译器无法识别这三个函数里的'year'、'month'、'day',认为它们是未定义的标识符。
[正确代码]
把第13、19、23行代码分别改为:
13 |
void Date::SetDate(int y, int m, int d) //设置日期值 |
19 |
bool Date::IsLeapYear( ) //判断是否闰年 |
23 |
void Date::Print( ) //输出 |
[运行结果]
闰年
平年
[例18.5] 定义“书”类和“出版社”类。“书”类中包含数据成员:“书名”、“作者”、“价格”、“出版社”等,并在成员函数Info里输出这些信息。“出版社”类包含数据成员“出版社名”,并在成员函数Publish里输出要出版哪一本文。
[错误代码]
1 |
#include <string.h> |
2 |
#include <iostream.h> |
3 |
|
4 |
class Book |
5 |
{ |
6 |
public: |
7 |
char name[50]; //书名 |
8 |
char author[50]; //作者 |
9 |
float price; //价格 |
10 |
Publisher* publisher; //出版社 |
11 |
void Info( ); |
12 |
}; |
13 |
class Publisher |
14 |
{ |
15 |
public: |
16 |
char name[50]; //出版社名称 |
17 |
void Publish(Book book) |
18 |
{ |
19 |
cout<<name<<"出版了:"<<book.name<<endl; |
20 |
} |
21 |
}; |
22 |
void Book::Info( ) //输出书的信息 |
23 |
{ |
24 |
cout<<"书名:"<<name<<endl; |
25 |
cout<<"作者:"<<author<<endl; |
26 |
cout<<"价格:"<<price<<endl; |
27 |
cout<<"出版商:"<<publisher->name<<endl; |
28 |
} |
29 |
|
30 |
void main(void) |
31 |
{ |
32 |
Publisher p; |
33 |
strcpy(p.name, "广州出版社"); |
34 |
|
35 |
Book b; |
36 |
strcpy(b.name, "天龙八部"); |
37 |
strcpy(b.author, "金庸"); |
38 |
b.price = 108; |
39 |
b.publisher = &p; |
40 |
|
41 |
p.Publish(b); |
42 |
b.Info( ); |
43 |
} |
[编译结果]
E:\CODE\18_5.cpp(10)
: error C2143: syntax error : missing ';' before '*'
E:\CODE\18_5.cpp(10)
: error C2501: 'Publisher' : missing storage-class or type specifiers
E:\CODE\18_5.cpp(10)
: error C2501: 'publisher' : missing storage-class or type specifiers
E:\CODE\18_5.cpp(27)
: error C2065: 'publisher' : undeclared identifier
E:\CODE\18_5.cpp(27)
: error C2227: left of '->name' must point to class/struct/union
E:\CODE\18_5.cpp(39)
: error C2039: 'publisher' : is not a member of 'Book'
E:\CODE\18_5.cpp(5) : see declaration of 'Book'
[问题分析]
在这个例子中,Book类中用到了Publisher类的对象publisher,Publisher类的成员函数void Publish(Book book)也用到了Book类的对象book。无论把那一个类的定义放在前面(本例中把Book类的定义放在前面),编译器都会报错,因为前面缺少另一个类的定义导致无法识别该类。所以,必须在两个类的定义之前,增加后面那个类的声明。在本例中,应该在Book类的定义之前(第3行代码处)增加如下语句:
class Publisher;
即可。
[正确代码]
把第3行代码改为:
3 |
class Publisher; |
[运行结果]
广州出版社出版了:天龙八部
书名:天龙八部
作者:金庸
价格:108
出版商:广州出版社
[例18.6] 定义“点”类,可以保留及显示该点的坐标值。
[错误代码]
1 |
#include <iostream.h> |
2 |
class Point |
3 |
{ |
4 |
public: |
5 |
int x; //横坐标 |
6 |
int y; //纵坐标 |
7 |
void Point( ) |
8 |
{ |
9 |
x = 0; |
10 |
y = 0; |
11 |
} |
12 |
void ~Point( ) |
13 |
{ |
14 |
} |
15 |
void PrintCoordinates( ) //输出该点坐标 |
16 |
{ |
17 |
cout<<"该点坐标为:("<<x<<","<<y<<")"<<endl; |
18 |
} |
19 |
}; |
20 |
|
21 |
void main(void) |
22 |
{ |
23 |
Point pt; |
24 |
pt.x = 5; |
25 |
pt.y = 8; |
26 |
pt.PrintCoordinates( ); |
27 |
} |
[编译结果]
E:\CODE\18_6.cpp(7)
: error C2380: type(s) preceding 'Point' (constructor with return type, or
illegal redefinition of current class-name?)
E:\CODE\18_6.cpp(13)
: error C2631: 'Point::~Point' : destructors not allowed a return type
[问题分析]
构造函数和析构函数不需要返回值,不能指定返回类型。
[正确代码]
把构造函数和析构函数的返回类型“void”去掉即可:
7 |
Point( ) |
……
12 |
~Point( ) |
[运行结果]
该点坐标为:(5,8)
[例18.7] 定义“矩形”类,可以保留及显示左上角、右下角两点的坐标值。
[错误代码]
1 |
#include <iostream.h> |
2 |
class Rect |
3 |
{ |
4 |
public: |
5 |
int left; //左上角横坐标 |
6 |
int top; //左上角纵坐标 |
7 |
int right; //右下角横坐标 |
8 |
int bottom; //右下角纵坐标 |
9 |
void PrintCoordinates( ) |
10 |
{ |
11 |
cout<<"左上角坐标为:("<<left<<","<<top<<")"<<endl; |
12 |
cout<<"右下角坐标为:("<<right<<","<<bottom<<")"<<endl; |
13 |
} |
14 |
Rect( ) |
15 |
{ |
16 |
left = 0; |
17 |
top = 0; |
18 |
right = 0; |
19 |
bottom = 0; |
20 |
} |
21 |
Rect(int x1,int y1,int x2,int y2) |
22 |
{ |
23 |
left = x1; |
24 |
top = y1; |
25 |
right = x2; |
26 |
bottom = y2; |
27 |
} |
28 |
~Rect( ) |
29 |
{ |
30 |
} |
31 |
~Rect(int x1,int y1,int x2,int y2) |
32 |
{ |
33 |
} |
34 |
}; |
35 |
|
36 |
void main(void) |
37 |
{ |
38 |
Rect rt(3,4,11,12); |
39 |
rt.PrintCoordinates( ); |
40 |
} |
[编译结果]
E:\CODE\18_7.cpp(32)
: error C2524: 'Rect' : destructors must have a 'void' formal parameter list
E:\CODE\18_7.cpp(34)
: error C2143: syntax error : missing ';' before '}'
E:\CODE\18_7.cpp(34)
: error C2143: syntax error : missing ';' before '}'
E:\CODE\18_7.cpp(34)
: error C2143: syntax error : missing ';' before '}'
E:\CODE\18_7.cpp(34)
: error C2143: syntax error : missing ';' before '}'
[问题分析]
一个类中只能定义一个析构函数,不能指定数据类型,而且不能有参数。本例中声明了了两个析构函数:
~Rect( )
~Rect(int x1,int y1,int x2,int y2)
而且第二个带有参数,是不合法的。
[正确代码]
删除第31
~ 33行代码即可。
[运行结果]
左上角坐标为:(3,4)
右下角坐标为:(11,12)
[例18.8] 定义“时间”类,可以保留及显示当前时间值(时、分、秒、毫秒)。
[错误代码]
1 |
#include <iostream.h> |
2 |
class Time |
3 |
{ |
4 |
private: |
5 |
int hour; //小时 |
6 |
int minute; //分 |
7 |
int second; //秒 |
8 |
int millisecond; //毫秒 |
9 |
public: |
10 |
Time(int h,int m,int s=0,int ms=0) |
11 |
{ |
12 |
hour = h; |
13 |
minute = m; |
14 |
second = s; |
15 |
millisecond = ms; |
16 |
} |
17 |
Time(int h,int m,int s) |
18 |
{ |
19 |
hour = h; |
20 |
minute = m; |
21 |
second = s; |
22 |
millisecond = 0; |
23 |
} |
24 |
void Print( ) |
25 |
{ |
26 |
cout<<hour<<":"<<minute<<":"<<second<<":"<<millisecond<<":"<<endl; |
27 |
} |
28 |
}; |
29 |
void main(void) |
30 |
{ |
31 |
Time t(12,30,30); |
32 |
t.Print( ); |
33 |
} |
[编译结果]
E:\CODE\18_8.cpp(31)
: error C2668: 'Time::Time' : ambiguous call to overloaded function
[问题分析]
本例中,构造函数有两种重载方式:
Time(int h,int m,int s=0,int ms=0)
Time(int h,int m,int s)
当前者只传递三个参数时,如第31行代码:
Time t(12,30,30);
就产生了二义性。编译器无法判断调用的是那一个构造函数。
[正确代码]
删除第17
~ 23行代码即可。
[运行结果]
12:30:30:0
[例18.9] 定义“圆”类。成员有半径、求面积等。
[错误代码]
1 |
#include <iostream.h> |
2 |
const double PI=3.14; |
3 |
class Circle |
4 |
{ |
5 |
public: |
6 |
float radius; //半径 |
7 |
Circle(float r) |
8 |
{ |
9 |
radius = r; |
10 |
} |
11 |
double Area( ) //求面积 |
12 |
{ |
13 |
return PI*radius*radius; |
14 |
} |
15 |
}; |
16 |
void main(void) |
17 |
{ |
18 |
Circle c; |
19 |
c.radius = 10; |
20 |
cout<<c.Area( )<<endl; |
21 |
} |
[编译结果]
E:\CODE\18_9.cpp(18)
: error C2512: 'Circle' : no appropriate default constructor available
[问题分析]
没有参数的构造函数称为默认构造函数。在一个类中,如果没有定义任何构造函数,则系统自动生成一个默认构造函数。不过本例中,已经定义了一个构造函数:
Circle(float r)
系统就不会自动生成默认构造函数。所以第18行代码
Circle c;
在编译时会报错:no appropriate default constructor
available。
[正确代码]
要解决这个问题,可以把第7行代码改为:
7 |
Circle(float r=0) |
即给构造函数Circle(float r)的参数r一个缺省值0。或者使用现有的构造函数,而把第18、19行代码改为:
18 |
Circle c(10); |
19 |
|
[运行结果]
314
[例18.10] 定义“整型数组”类,在构造函数里根据给定的数组大小动态分配内存,在析构函数里释放内存。
[错误代码]
1 |
#include <iostream.h> |
2 |
class IntArray |
3 |
{ |
4 |
public: |
5 |
int size; //数组大小 |
6 |
int *p; |
7 |
IntArray(int s) |
8 |
{ |
9 |
size = s; |
10 |
p = new int[size]; |
11 |
if(p) |
12 |
for(int i=0;i<size;i++) //把数组元素初始化为0 |
13 |
p[i] = 0; |
14 |
} |
15 |
~IntArray( ) |
16 |
{ |
17 |
cout<<"销毁对象!"<<endl; |
18 |
delete p; |
19 |
} |
20 |
void SetItem(int index, int value) //为数组元素赋值 |
21 |
{ |
22 |
if(p) |
23 |
if(index>=0 && index<size) |
24 |
p[index] = value; |
25 |
} |
26 |
void print( ) //输出 |
27 |
{ |
28 |
if(p) |
29 |
for(int i=0;i<size;i++) |
30 |
cout<<p[i]<<"\t"<<endl; |
31 |
} |
32 |
}; |
33 |
void main(void) |
34 |
{ |
35 |
IntArray intArray(5); |
36 |
intArray.SetItem(1,19); |
37 |
intArray.SetItem(2,108); |
38 |
intArray.SetItem(4,88); |
39 |
|
40 |
IntArray intArray2(intArray); |
41 |
intArray2.print( ); |
42 |
} |
[编译结果]
编译通过。但运行时出错:
图18.1默认的拷贝构造函数导致的内存问题
[问题分析]
main函数里先建立了IntArray类的对象intArray并为之赋值,然后,语句
IntArray intArray2(intArray);
调用了默认的拷贝构造函数创建了对象intArray2。通过调用对象intArray2的print方法输出它的元素值。通过运行结果可见,在程序结束前,对象intArray和对象intArray2分别执行了析构函数(输出字符串“销毁对象!”),然后程序报错。可见问题发生在第二次执行了析构函数时。
实际上默认的拷贝构造函数是浅拷贝构造函数。即如果一个类拥有资源(堆,或者是其它系统资源),当这个类的对象发生复制过程的时候并未复制资源,这种情况视为浅拷贝。对象intArray在创建时为数据成员p动态分配了内存;在调用了默认的拷贝构造函数创建了对象intArray2时,却没有为对象intArray2的数据成员p动态分配内存。对象intArray和对象intArray2的数据成员p指向同一块内存空间。对象intArray和对象intArray2在析构时都执行了delete p; 所以在第二次执行该语句时程序报错。
[正确代码]
要解决这个问题,必须自行定义拷贝构造函数,即深拷贝构造函数,使得在调用拷贝构造函数创建对象intArray2时,为对象intArray2的数据成员p重新动态分配内存,并复制对象intArray里的数组元素值。
在代码第15行处插入以下代码:
15 |
IntArray(IntArray &pObject) |
16 |
{ |
17 |
size = pObject.size; |
18 |
p = new int[size]; |
19 |
if(p) |
20 |
for(int i=0;i<size;i++) //复制数组 |
21 |
p[i] = pObject.p[i]; |
22 |
} |
[运行结果]
0
19
108
0
88
销毁对象!
销毁对象!
[例18.11] 创建“员工”类,用静态数据成员表示员工的总数。
[错误代码]
1 |
#include <iostream.h> |
2 |
class Employee |
3 |
{ |
4 |
private: |
5 |
static int Count; //用静态数据成员表示员工的总数 |
6 |
public: |
7 |
Employee( ) |
8 |
{ |
9 |
Count++; |
10 |
} |
11 |
~Employee( ) |
12 |
{ |
13 |
Count--; |
14 |
} |
15 |
int GetCount( ) //返回员工的总数 |
16 |
{ |
17 |
return Count; |
18 |
} |
19 |
}; |
20 |
void main( ) |
21 |
{ |
22 |
Employee e1,e2,e3; |
23 |
cout<<"共有"<<e1.GetCount( )<<"名员工。"<<endl; |
24 |
cout<<"共有"<<e2.GetCount( )<<"名员工。"<<endl; |
25 |
cout<<"共有"<<e3.GetCount( )<<"名员工。"<<endl; |
26 |
} |
[编译结果]
Linking...
18_11.obj
: error LNK2001: unresolved external symbol "private: static int
Employee::Count" (?Count@Employee@@
Debug/18_11.exe
: fatal error LNK1120: 1 unresolved externals
Error
executing link.exe.
[问题分析]
第5行代码
static int Count;
声明了静态数据成员Count,表示员工的总数。但是:
(1)静态数据成员必须初始化。在本例中静态数据成员Count没有初始化。
(2)静态数据成员在类体外初始化,形式为:<类名>::<静态成员名>。
[正确代码]
在第20行插入如下代码:
20 |
int Employee::Count = 0; |
[运行结果]
共有3名员工。
共有3名员工。
共有3名员工。
[例18.12] 定义“数学”类,其静态成员函数square可以计算一个整数的平方。
[错误代码]
1 |
#include <iostream.h> |
2 |
class Math |
3 |
{ |
4 |
private: |
5 |
int data; |
6 |
public: |
7 |
Math(int d) |
8 |
{ |
9 |
data = d; |
10 |
} |
11 |
static int square( ) //求平方 |
12 |
{ |
13 |
return data*data; |
14 |
} |
15 |
}; |
16 |
void main( ) |
17 |
{ |
18 |
Math m(5); |
19 |
cout<< m.square( )<<endl; |
20 |
} |
[编译结果]
E:\Code\18_12.cpp(13)
: error C2597: illegal reference to data member 'Math::data' in a static member
function
E:\Code\18_12.cpp(13)
: error C2597: illegal reference to data member 'Math::data' in a static member
function
E:\Code\18_12.cpp(13)
: error C2568: '*' : unable to resolve function overload
[问题分析]
在静态成员函数中,可以直接引用类中说明的静态成员和全局变量,而不允许直接引用类中说明的非静态数据成员。在本例中,
static int square( )
是静态成员函数。在静态成员函数square中,试图直接引用data,这是非法的。
[正确代码]
1 |
#include <iostream.h> |
2 |
class Math |
3 |
{ |
4 |
public: |
5 |
static int square(int data) //求平方 |
6 |
{ |
7 |
return data*data; |
8 |
} |
9 |
}; |
10 |
void main( ) |
11 |
{ |
12 |
cout<<Math::square(5)<<endl; |
13 |
} |
[运行结果]
25
[例18.13] 写一个函数ToUpper,把一个给定的字符串转换为大写。在ToUpper里,定义一个局部类MyChar,用以把一个给定字符转换为大写。
[错误代码]
1 |
#include <string.h> |
2 |
#include <iostream.h> |
3 |
char* ToUpper(char* str) |
4 |
{ |
5 |
class MyChar //局部类 |
6 |
{ |
7 |
public: |
8 |
static char ToUpper(char ch); //把一个给定字符转换为大写 |
9 |
}; |
10 |
int len = strlen(str); |
11 |
char* p = new char[len+1]; |
12 |
for(int i=0;i<len;i++) //把字符串str中的字符依次转换为大写 |
13 |
{ |
14 |
p[i] = MyChar::ToUpper(str[i]); |
15 |
} |
16 |
p[len] = '\0'; |
17 |
return p; |
18 |
} |
19 |
char MyChar::ToUpper(char ch) |
20 |
{ |
21 |
return (ch>='a' && ch<='z') ? (ch-32) : ch; |
22 |
} |
23 |
void main( ) |
24 |
{ |
25 |
char* p = ToUpper("Hello,world!"); |
26 |
cout<<p<<endl; |
27 |
delete p; |
28 |
} |
[编译结果]
E:\Code\18_13.cpp(8)
: error C2599: 'ToUpper' : local class member functions must be defined within
the class
E:\Code\18_13.cpp(8) : see declaration of 'ToUpper'
E:\Code\18_13.cpp(19)
: error C2653: 'MyChar' : is not a class or namespace name
[问题分析]
局部类不能说明静态成员函数,而且所有成员函数都必须定义在类体里。
[正确代码]
1 |
#include <string.h> |
2 |
#include <iostream.h> |
3 |
char* ToUpper(char* str) |
4 |
{ |
5 |
class MyChar |
6 |
{ |
7 |
private: |
8 |
char ch; |
9 |
public: |
10 |
MyChar(char c) |
11 |
{ |
12 |
ch = c; |
13 |
} |
14 |
char ToUpper( ) //把一个给定字符转换为大写 |
15 |
{ |
16 |
return (ch>='a' && ch<='z') ? (ch-32) : ch; |
17 |
} |
18 |
}; |
19 |
int len = strlen(str); |
20 |
char* p = new char[len+1]; |
21 |
for(int i=0;i<len;i++) //把字符串str中的字符依次转换为大写 |
22 |
{ |
23 |
MyChar
myChar(str[i]); |
24 |
p[i]
= myChar.ToUpper( ); |
25 |
} |
26 |
p[len] = '\0'; |
27 |
return p; |
28 |
} |
29 |
void main( ) |
30 |
{ |
31 |
char* p = ToUpper("Hello,world!"); |
32 |
cout<<p<<endl; |
33 |
delete p; |
34 |
} |
[运行结果]
HELLO,WORLD!
[例18.14] 定义一个类MyString,其成员函数ToUpper可以把一个给定的字符串转换为大写。在类MyString里,定义一个嵌套类MyChar,用以把一个给定字符转换为大写。
[错误代码]
1 |
#include <string.h> |
2 |
#include <iostream.h> |
3 |
class MyString |
4 |
{ |
5 |
public: |
6 |
class MyChar //嵌套类 |
7 |
{ |
8 |
private: |
9 |
char ch; |
10 |
public: |
11 |
MyChar(char c); |
12 |
char ToUpper( ); //把一个给定字符转换为大写 |
13 |
}; |
14 |
|
15 |
char* str; |
16 |
MyString(char* p); |
17 |
char* ToUpper( ); |
18 |
}; |
19 |
|
20 |
MyChar::MyChar(char c) |
21 |
{ |
22 |
ch = c; |
23 |
} |
24 |
|
25 |
char MyChar::ToUpper( ) |
26 |
{ |
27 |
return (ch>='a' && ch<='z') ? (ch-32) : ch; |
28 |
} |
29 |
|
30 |
MyString::MyString(char* p) |
31 |
{ |
32 |
str = p; |
33 |
} |
34 |
|
35 |
char* MyString::ToUpper( ) |
36 |
{ |
37 |
int len = strlen(str); |
38 |
char* p = new char[len+1]; |
39 |
for(int i=0;i<len;i++) //把字符串str中的字符依次转换为大写 |
40 |
{ |
41 |
MyChar myChar(str[i]); |
42 |
p[i] = myChar.ToUpper( ); |
43 |
} |
44 |
p[len] = '\0'; |
45 |
return p; |
46 |
} |
47 |
|
48 |
void main( ) |
49 |
{ |
50 |
MyString myString("Hello,world!"); |
51 |
char* p = myString.ToUpper( ); |
52 |
cout<<"把字符串Hello,world!转换为大写:"<<p<<endl; |
53 |
delete p; |
54 |
|
55 |
MyChar myChar('a'); |
56 |
cout<<"把字符a转换为大写:"<<myChar.ToUpper( )<<endl; |
57 |
} |
[编译结果]
E:\Code\18_14.cpp(20)
: error C2653: 'MyChar' : is not a class or namespace name
E:\Code\18_14.cpp(22)
: error C2065: 'ch' : undeclared identifier
E:\Code\18_14.cpp(23)
: warning C4508: 'MyChar' : function should return a value; 'void' return type
assumed
E:\Code\18_14.cpp(25)
: error C2653: 'MyChar' : is not a class or namespace name
E:\Code\18_14.cpp(55)
: error C2146: syntax error : missing ';' before identifier 'myChar'
E:\Code\18_14.cpp(55)
: warning C4551: function call missing argument list
E:\Code\18_14.cpp(55)
: error C2065: 'myChar' : undeclared identifier
E:\Code\18_14.cpp(56)
: error C2228: left of '.ToUpper' must have class/struct/union type
[问题分析]
从作用域的角度看,嵌套类被隐藏在外围类之中,该类名只能在外围类中使用。如果在外围类的作用域外使用该类名时,需要加名字限定。在本例中,外围类是MyString,嵌套类是MyChar。第55行代码
MyChar myChar('a');
是非法的。因为在main函数里使用嵌套类MyChar,需要加名字限定,如:
MyString::MyChar myChar('a');
另外,嵌套类中的成员函数不可以在它的类体外定义。
[正确代码]
需要把嵌套类MyChar中的构造函数和成员函数ToUpper放到类体里定义,还要把第55行代码加上名字限定:
MyString::MyChar myChar('a');
正确代码代码如下:
1 |
#include <string.h> |
2 |
#include <iostream.h> |
3 |
class MyString |
4 |
{ |
5 |
public: |
6 |
class MyChar //嵌套类 |
7 |
{ |
8 |
private: |
9 |
char ch; |
10 |
public: |
11 |
MyChar(char
c) |
12 |
{
|
13 |
ch
= c; |
14 |
}
|
15 |
char
ToUpper( ) //把一个给定字符转换为大写 |
16 |
{
|
17 |
return
(ch>='a' && ch<='z') ? (ch-32) : ch; |
18 |
}
|
19 |
}; |
20 |
|
21 |
char* str; |
22 |
MyString(char* p); |
23 |
char* ToUpper( ); |
24 |
}; |
25 |
MyString::MyString(char* p) |
26 |
{ |
27 |
str = p; |
28 |
} |
29 |
char* MyString::ToUpper( ) |
30 |
{ |
31 |
int len = strlen(str); |
32 |
char* p = new char[len+1]; |
33 |
for(int i=0;i<len;i++) //把字符串str中的字符依次转换为大写 |
34 |
{ |
35 |
MyChar myChar(str[i]); |
36 |
p[i] = myChar.ToUpper( ); |
37 |
} |
38 |
p[len] = '\0'; |
39 |
return p; |
40 |
} |
41 |
void main( ) |
42 |
{ |
43 |
MyString myString("Hello,world!"); |
44 |
char* p = myString.ToUpper( ); |
45 |
cout<<"把字符串Hello,world!转换为大写:"<<p<<endl; |
46 |
delete p; |
47 |
|
48 |
MyString::MyChar myChar('a'); |
49 |
cout<<"把字符a转换为大写:"<<myChar.ToUpper( )<<endl; |
50 |
} |
[运行结果]
把字符串Hello,world!转换为大写:HELLO,WORLD!
把字符a转换为大写:A
请改正如下程序中的错误。
1) 定义“硬盘”类。
1 |
#include <string> |
2 |
#include <iostream> |
3 |
using namespace std; |
4 |
|
5 |
class HardDisk |
6 |
{ |
7 |
public: |
8 |
string brand; //品牌 |
9 |
int size; //硬盘容量(GB) |
10 |
int speed; //转速(转) |
11 |
int cashsize; //缓存大小(MB) |
12 |
public: |
13 |
void Display( ); |
14 |
}; |
15 |
|
16 |
void HardDisk::Display( ) |
17 |
{ |
18 |
cout<<"品牌:"<<brand<<endl; |
19 |
cout<<"容量:"<<size<<"GB"<<endl; |
20 |
cout<<"转速:"<<speed<<endl; |
21 |
cout<<"缓存大小:"<<cashsize<<"MB"<<endl; |
22 |
} |
23 |
|
24 |
void main( ) |
25 |
{ |
26 |
HardDisk hd; |
27 |
hd.brand = "WestDigital"; |
28 |
hd.size = 160; |
29 |
hd.speed =5400; |
30 |
hd.cashsize = 2; |
31 |
hd.Display( ); |
32 |
} |
2) 定义“硬盘”类以及“工厂”类,注意二者的关系。
1 |
#include <string.h> |
2 |
#include <iostream.h> |
3 |
|
4 |
class HardDisk |
5 |
{ |
6 |
public: |
7 |
char brand[50]; //品牌 |
8 |
int size; //硬盘容量(GB) |
9 |
int speed; //转速(转) |
10 |
int cashsize; //缓存大小(MB) |
11 |
Factory* producer; //生产工厂 |
12 |
void Display( ); |
13 |
}; |
14 |
|
15 |
class Factory |
16 |
{ |
17 |
public: |
18 |
char name[50]; |
19 |
void Produce(HardDisk& hdd) |
20 |
{ |
21 |
cout<<"产品信息:"<<endl; |
22 |
hdd.producer = this; |
23 |
hdd.Display( ); |
24 |
} |
25 |
}; |
26 |
|
27 |
void Display( ) |
28 |
{ |
29 |
cout<<"品牌:"<<brand<<endl; |
30 |
cout<<"容量:"<<size<<"GB"<<endl; |
31 |
cout<<"转速:"<<speed<<endl; |
32 |
cout<<"缓存大小:"<<cashsize<<"MB"<<endl; |
33 |
cout<<"生产厂家:"<<producer->name<<endl; |
34 |
} |
35 |
|
36 |
void main( ) |
37 |
{ |
38 |
HardDisk hd; |
39 |
strcpy(hd.brand, "West Digital"); |
40 |
hd.size = 160; |
41 |
hd.speed =5400; |
42 |
hd.cashsize = 2; |
43 |
|
44 |
Factory f; |
45 |
strcpy(f.name, "West Digital"); |
46 |
f.Produce(hd); |
47 |
} |
3) 定义“空调”类。
1 |
#include "string.h" |
2 |
#include "iostream.h" |
3 |
class AirConditioner |
4 |
{ |
5 |
protected: |
6 |
char* m_pBrand; //品牌 |
7 |
char* m_pModel; //型号 |
8 |
int Power; //功率 |
9 |
float Price; //价格 |
10 |
public: |
11 |
void AirConditioner(char* pBrand,char* pModel,int nPower,float fPrice); |
12 |
void ~AirConditioner(int a); |
13 |
void ~AirConditioner( ); |
14 |
void Refrigeration( ); |
15 |
void Heat( ); |
16 |
void Desc( ); |
17 |
void Shutdown( ); |
18 |
}; |
19 |
void AirConditioner::AirConditioner(char* pBrand,char* pModel,int nPower,float fPrice) |
20 |
{ |
21 |
m_pBrand = new char[50]; |
22 |
m_pModel = new char[50]; |
23 |
strcpy(m_pBrand,pBrand); |
24 |
strcpy(m_pModel,pModel); |
25 |
Power = nPower; |
26 |
Price = fPrice; |
27 |
} |
28 |
void AirConditioner::~AirConditioner( ) |
29 |
{ |
30 |
delete m_pBrand; |
31 |
delete m_pModel; |
32 |
} |
33 |
void AirConditioner::~AirConditioner(int a) |
34 |
{ |
35 |
} |
36 |
void AirConditioner::Shutdown( ) |
37 |
{ |
38 |
cout<<"关机!"<<endl; |
39 |
} |
40 |
void AirConditioner::Refrigeration( ) |
41 |
{ |
42 |
cout<<"开始制冷!"<<endl; |
43 |
} |
44 |
void AirConditioner::Heat( ) |
45 |
{ |
46 |
cout<<"开始制热!"<<endl; |
47 |
} |
48 |
void AirConditioner::Desc( ) |
49 |
{ |
50 |
cout<<"品牌:"<<m_pBrand<<endl; |
51 |
cout<<"型号:"<<m_pModel<<endl; |
52 |
cout<<"功率:"<<Power<<endl; |
53 |
cout<<"价格:"<<Price<<endl; |
54 |
} |
55 |
|
56 |
void main( ) |
57 |
{ |
58 |
AirConditioner ac; |
59 |
ac.Desc( ); |
60 |
ac.Refrigeration( ); |
6 |
ac.Heat( ); |
62 |
ac.Shutdown( ); |
63 |
} |
4) 定义一个类用于字符串操作。
1 |
#include "string.h" |
2 |
#include "iostream.h" |
3 |
class String |
4 |
{ |
5 |
protected: |
6 |
char* m_pString; |
7 |
public: |
8 |
String( ); |
9 |
String(char* pString=""); |
10 |
~String( ); |
11 |
void Append(char* pString); |
12 |
void print( ); |
13 |
}; |
14 |
String::String( ) |
15 |
{ |
16 |
m_pString = new char[128]; |
17 |
m_pString[0]='\0'; |
18 |
} |
19 |
String::String(char* pString) |
20 |
{ |
21 |
m_pString = new char[128]; |
22 |
strcpy(m_pString,pString); |
23 |
} |
24 |
String::~String( ) |
25 |
{ |
26 |
delete m_pString; |
27 |
} |
28 |
void String::Append(char* pString) |
29 |
{ |
30 |
strcat(m_pString,pString); |
31 |
} |
32 |
void String::print( ) |
33 |
{ |
34 |
cout<<m_pString<<endl; |
35 |
} |
36 |
void main( ) |
37 |
{ |
38 |
String s; |
39 |
s.Append("Hello"); |
40 |
s.Append(" World!"); |
41 |
s.print( ); |
42 |
|
43 |
String t(s); |
44 |
t.print( ); |
45 |
} |
1.友元的概念和定义
使用friend关键字,友元提供了在不同类的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制。通过友元,一个普通函数或另一个类中的成员函数可以访问类中的私有成员和保护成员。友元破坏了类的封装性和数据的隐蔽性,导致程序可维护性变差,但正确使用的话能提高程序的运行效率。友元分为友元函数、友元成员和友元类三种。
2.友元函数
友元函数是一种说明在类定义体内的非成员函数。定义格式如下:
friend 〈返回值类型〉〈函数名〉(〈参数表〉);
需要注意以下几点:
友元函数是在类中说明的一个函数,它不是该类的成员函数,但允许访问该类的所有成员。
由于友元函数不是类的成员,所以没有this指针,访问该类的对象的成员时,必须使用对象名,而不能直接使用类的成员名。
友元说明可以代替该函数的函数说明。
如果在说明友元时给出了该函数的函数体代码,则它是内联的。
在类外定义友元函数时去掉friend关键字。
3.友元成员
另一个类的成员函数可以作为某个类的友元,只需在声明友元函数时加上成员函数所在的类名,称为友元成员。声明如下:
friend 〈返回值类型〉 〈类名〉∷〈成员函数名〉(〈参数表〉) ;
与友元函数比较,友元成员的存取范围要小得多,因为这里的友元函数只是一个类中的一个成员函数,friend授权该函数可以访问宣布其为友元的类中的所有成员。
4.友元类
某一个类可以是另一个类的友元,称为友元类。友元类中的所有成员函数都可以访问另一个类中的所有成员。其说明方式如下:
friend class 〈类名〉;
[例19.1] 定义一个Point类,能保存并打印一个平面上的点的坐标。用友元函数计算两点之间的距离。
[错误代码]
1 |
#include <math.h> |
2 |
#include <iostream.h> |
3 |
class Point |
4 |
{ |
5 |
private: |
6 |
double x; //横坐标 |
7 |
double y; //纵坐标 |
8 |
public: |
9 |
Point(double x0, double y0) |
10 |
{ |
11 |
x = x0; |
12 |
y = y0; |
13 |
} |
14 |
void PrintCoordinates( ) //输出坐标 |
15 |
{ |
16 |
cout<<"("<<x<<","<<y<<")"<<endl; |
17 |
} |
18 |
friend double Distance(Point &a); //计算两点之间的距离 |
19 |
}; |
20 |
|
21 |
double Distance(Point &a) //计算两点之间的距离 |
22 |
{ |
23 |
double dx = a.x - x; |
24 |
double dy = a.y - y; |
25 |
return sqrt(dx*dx+dy*dy); |
26 |
} |
27 |
|
28 |
void main(void) |
29 |
{ |
30 |
Point p1(3.0,4.0); |
31 |
Point p2(6.0,8.0); |
32 |
p1.PrintCoordinates( ); |
33 |
p2.PrintCoordinates( ); |
34 |
double d = p1.Distance(p2); |
35 |
cout<<"两点之间距离为"<<d<<endl; |
36 |
} |
[编译结果]
E:\Code\19_1.cpp(23)
: error C2065: 'x' : undeclared identifier
E:\Code\19_1.cpp(24)
: error C2065: 'y' : undeclared identifier
E:\Code\19_1.cpp(34)
: error C2039: 'Distance' : is not a member of 'Point'
E:\Code\19_1.cpp(4) : see declaration of 'Point'
[问题分析]
友元函数是在类中说明的一个函数,它不是该类的成员函数,但允许访问该类的所有成员。它是独立于任何类的一般的外部函数。友元并不在类的范围中,它们也不需使用成员选择符(.或->)调用,除非它们是其它类的成员。本例中第34行代码中p1.Distance(p2)正是犯了这个错误。
由于友元函数不是类的成员,所以没有this指针,访问该类的对象的成员时,必须使用对象名,而不能直接使用类的成员名。在本例中,友元函数:
double Distance(Point &a)
中,把自己当成Point类的成员,试图访问Point类的数据成员x、y,是错误的。该函数是独立于Point类的一般的外部函数。所以计算距离的两个点都应该作为参数传进来。
[正确代码]
18 |
friend double Distance(Point &a, Point &b) //计算两点之间的距离 |
……
21 |
double Distance(Point &a, Point &b) //计算两点之间的距离 |
22 |
{ |
23 |
double dx = a.x - b.x; |
24 |
double dy = a.y - b.y; |
25 |
return sqrt(dx*dx+dy*dy); |
26 |
} |
27 |
|
……
34 |
double d = Distance(p1,p2); |
……
[运行结果]
(3,4)
(6,8)
两点之间距离为5
[例19.2] 定义一个“球体”类,用友元函数计算其体积。
[错误代码]
1 |
#include <iostream.h> |
2 |
const double PI=3.14; |
3 |
class Ball |
4 |
{ |
5 |
private: |
6 |
float radius; //半径 |
7 |
public: |
8 |
Ball(float r) |
9 |
{ |
10 |
radius = r; |
11 |
} |
12 |
void Print( ) |
13 |
{ |
14 |
cout<<"球体的半径为"<<radius<<endl; |
15 |
} |
16 |
friend double Volume(Ball &b); //计算球体的体积 |
17 |
}; |
18 |
|
19 |
friend double Volume(Ball &b) //计算球体的体积 |
20 |
{ |
21 |
return PI*b.radius*b.radius*b.radius*4.0/3.0; |
22 |
} |
23 |
|
24 |
void main(void) |
25 |
{ |
26 |
Ball b(10); |
27 |
b.Print( ); |
28 |
cout<<"球体的体积为"<<Volume(b)<<endl; |
29 |
} |
[编译结果]
E:\Code\19_2.cpp(20)
: error C2255: 'Volume' : a friend function can only be declared in a class
E:\Code\19_2.cpp(20)
: error C2556: 'void __cdecl Volume(class Ball &)' : overloaded function
differs only by return type from 'double __cdecl Volume(class Ball &)'
E:\Code\19_2.cpp(16) : see declaration of 'Volume'
[问题分析]
在类外定义友元函数时应该去掉friend关键字。第18行代码中的friend关键字是多余的。
[正确代码]
把第19行代码改为
19 |
double Volume(Ball &b) |
[运行结果]
球体的半径为10
球体的体积为4186.67
[例19.3] 定义“学生”类和“成绩”类,可以输出姓名、学号,以及数学、语文、英语成绩。
[错误代码]
1 |
#include<iostream.h> |
2 |
#include<string.h> |
3 |
class Student; |
4 |
class Score //“成绩”类 |
5 |
{ |
6 |
private: |
7 |
int math,chinese,english; //数学、语文、英语成绩 |
8 |
public: |
9 |
Score(int m, int c, int e) |
10 |
{ |
11 |
math = m; |
12 |
chinese = c; |
13 |
english = e; |
14 |
} |
15 |
}; |
16 |
class Student //“学生”类 |
17 |
{ |
18 |
private: |
19 |
friend class Score; //成绩 |
20 |
char name[10]; //姓名 |
21 |
char ID[10]; //学号 |
22 |
public: |
23 |
void ShowStudent( ) //输出姓名、学号 |
24 |
{ |
25 |
cout<<"姓名:"<<name<<endl; |
26 |
cout<<"学号:"<<ID<<endl; |
27 |
} |
28 |
void ShowScore(Score& sr) //输出数学、语文、英语成绩 |
29 |
{ |
30 |
cout<<"数学:"<<sr.math<<endl; |
31 |
cout<<"语文:"<<sr.chinese<<endl; |
32 |
cout<<"英语:"<<sr.english<<endl; |
33 |
} |
34 |
Student(char *sName,char *sID) |
35 |
{ |
36 |
strcpy(name,sName); |
37 |
strcpy(ID,sID); |
38 |
} |
39 |
}; |
40 |
void main( ) |
41 |
{ |
42 |
Student wang("小王","021100101"); |
43 |
Score score(72,82,92); |
44 |
wang.ShowStudent( ); |
45 |
wang.ShowScore(score); |
46 |
} |
[编译结果]
E:\Code\19_3.cpp(30)
: error C2248: 'math' : cannot access private member declared in class 'Score'
E:\Code\19_3.cpp(7) : see declaration of 'math'
E:\Code\19_3.cpp(31)
: error C2248: 'chinese' : cannot access private member declared in class
'Score'
E:\Code\19_3.cpp(7) : see declaration of 'chinese'
E:\Code\19_3.cpp(32)
: error C2248: 'english' : cannot access private member declared in class
'Score'
E:\Code\19_3.cpp(7) : see declaration of 'english'
[问题分析]
第19行代码
friend class Score;
说明了Score类是Student类的友元类,因此,Score类的成员函数都是Student类的友元函数,都可以访问Student类的私有成员。但友元关系是单向的,不具有交换性。程序中并没有说明Student类是Score类的友元类。而Student类中的ShowScore函数试图访问Score类的私有成员math,chinese和english,是非法的。
[正确代码]
如果希望Student类中的ShowScore函数有权访问Score类的私有成员math、chinese和english,就必须在Score类中说明Student类是Score类的友元类。第19行代码可以去掉,另外,在第7行插入如下代码:
7 |
friend class Student; |
[运行结果]
姓名:小王
学号:021100101
数学:72
语文:82
英语:92
[例19.4] 定义ClassA、ClassB、ClassC三个类,要求ClassB是ClassA的友元类,ClassC是ClassA、ClassB的友元类。
[错误代码]
1 |
#include<iostream.h> |
2 |
#include<string.h> |
3 |
class ClassA |
4 |
{ |
5 |
private: |
6 |
friend class ClassB; |
7 |
int propA; |
8 |
public: |
9 |
ClassA(int a) |
10 |
{ |
11 |
propA = a; |
12 |
} |
13 |
}; |
14 |
class ClassB |
15 |
{ |
16 |
private: |
17 |
friend class ClassC; |
18 |
int propB; |
19 |
public: |
20 |
ClassB(int b) |
21 |
{ |
22 |
propB = b; |
23 |
} |
24 |
void PrintA(ClassA &ca) |
25 |
{ |
26 |
cout<<"B>>>A:"<<ca.propA<<endl; |
27 |
} |
28 |
}; |
29 |
class ClassC |
30 |
{ |
31 |
private: |
32 |
int propC; |
33 |
public: |
34 |
ClassC(int c) |
35 |
{ |
36 |
propC = c; |
37 |
} |
38 |
void PrintA(ClassA &ca) |
39 |
{ |
40 |
cout<<"C>>>A:"<<ca.propA<<endl; |
41 |
} |
42 |
void PrintB(ClassB &cb) |
43 |
{ |
44 |
cout<<"C>>>B:"<<cb.propB<<endl; |
45 |
} |
46 |
}; |
47 |
|
48 |
void main( ) |
49 |
{ |
50 |
ClassA ca(10); |
51 |
ClassB cb(20); |
52 |
ClassC cc(30); |
53 |
cb.PrintA(ca); |
54 |
cc.PrintA(ca); |
55 |
cc.PrintB(cb); |
56 |
} |
[编译结果]
E:\Code\19_4.cpp(40)
: error C2248: 'propA' : cannot access private member declared in class
'ClassA'
E:\Code\19_4.cpp(7) : see declaration of 'propA'
[问题分析]
友元关系不具有传递性。ClassB是ClassA的友元类,ClassC是ClassB的友元类,并不意味着ClassC是ClassA的友元类。必须在ClassA中说明。
[正确代码]
在第6行代码处插入如下代码:
6 |
friend class ClassC; |
[运行结果]
B>>>A:10
C>>>A:10
C>>>B:20
请改正如下程序中的错误。
1) 定义“网站”类和“国家”类,使用友元函数输出相关信息。
1 |
#include <iostream.h> |
2 |
#include <string.h> |
3 |
class Website; |
4 |
class Country //“国家”类 |
5 |
{ |
6 |
public: |
7 |
Country( ) |
8 |
{ |
9 |
strcpy(name,"中国"); |
10 |
} |
11 |
friend void ShowInfo(Website &website,Country &cn); |
12 |
protected: |
13 |
char name[30]; |
14 |
}; |
15 |
class Website //“网站”类 |
16 |
{ |
17 |
public: |
18 |
Website(char *name,char *url) |
19 |
{ |
20 |
strcpy(this->name,name); |
21 |
strcpy(this->url,url); |
22 |
} |
23 |
friend void ShowInfo(Website &website,Country &cn); |
24 |
public: |
25 |
char name[20]; |
26 |
char url[20]; |
27 |
}; |
28 |
friend void Website::ShowInfo(Website &website,Country &cn) //输出 |
29 |
{ |
30 |
cout<<cn.name<<endl; |
31 |
cout<<website.name<<endl; |
32 |
cout<<website.url<<endl; |
33 |
} |
34 |
void main( ) |
35 |
{ |
36 |
Website w("上海工程技术大学","www.sues.edu.cn"); |
37 |
Country c; |
38 |
ShowInfo(w,c); |
39 |
} |
2) 定义“网站”类和“国家”类,使用友元类输出相关信息。
1 |
#include <iostream.h> |
2 |
#include <string.h> |
3 |
class Country; |
4 |
class Website //“网站”类 |
5 |
{ |
6 |
public: |
7 |
Website(char *name,char *url) |
8 |
{ |
9 |
strcpy(this->name,name); |
10 |
strcpy(this->url,url); |
11 |
} |
12 |
void ShowInfo(Country& country); //输出“国家” |
13 |
friend class Country; |
14 |
public: |
15 |
char name[20]; |
16 |
char url[20]; |
17 |
}; |
18 |
class Country //“国家”类 |
19 |
{ |
20 |
public: |
21 |
Country( ) |
22 |
{ |
23 |
strcpy(name,"中国"); |
24 |
} |
25 |
protected: |
26 |
char name[30]; |
27 |
}; |
28 |
void Website::ShowInfo(Country& country) //输出“国家”和“网站”信息 |
29 |
{ |
30 |
cout<<country.name<<endl; |
31 |
cout<<name<<endl; |
32 |
cout<<url<<endl; |
33 |
} |
34 |
void main( ) |
35 |
{ |
36 |
Country c; |
37 |
Website w("上海工程技术大学","www.sues.edu.cn"); |
38 |
w.ShowInfo(c); |
39 |
} |
继承性是面向对象程序设计的重要特性之一。通过继承实现了数据抽象基础上的代码重用,减少代码冗余,简化接口和界面。继承性反映了类的层次结构。继承性使程序员可以从一个或多个已定义的类中继承数据成员和成员函数,也可以重新定义或加进新的数据成员和成员函数,以建立新类。这个新类称为派生类或子类,而已有的类称为基类、超类或父类。
1. 单继承
在单继承中,每个类可以有多个派生类,但是每个派生类只能有一个基类,从而形成树形结构。在单继承的情况下,类的定义格式如下:
class<派生类名>:[继承方式] <基类名>
{
// 派生类成员声明;
};
其中,class是类声明的关键字,用于告诉编译器下面声明的是一个类。派生类名是新生成的类名。继承方式规定了如何访问从基类继承的成员,包括私有继承、公有继承和保护继承,其关键字分别为private、public和protected。这里的派生类成员指除了从基类继承来的成员之外,新增加的数据成员和成员函数。
2. 多继承
一个派生类指定多个基类,这样的继承结构被称做多继承。在多继承的情况下,类的定义格式如下:
class<派生类名>:[继承方式]
基类名1,[继承方式]
基类名2,...,[继承方式] 基类名n
{
// 派生类成员声明;
};
多继承可以看作是单继承的扩展,而单继承可以看作是多继承的一个最简单的特例。多继承有时会导致二义性的问题,例如:
(1)假如多继承时基类的成员有成员名相同的情况,如果使用一个表达式引用了这些同名的成员,就会造成无法确定是引用哪个基类的成员,导致二义性。可以在成员名前用对象名及基类名来限定。
(2) 如果一个派生类从多个基类中派生,而这些基类又有一个共同的基类,则在此派生类中访问这个共同基类中的成员时会产生二义性。可以利用虚基类来避免。
3. 类的继承方式
类的继承方式规定了如何访问从基类继承的成员,包括私有继承、公有继承和保护继承三种。具体如下:
(1) 无论哪一种继承方式,基类的私有成员在派生类中均是不可访问的,只能由基类的成员函数访问。
(2) 在公有继承方式下,基类中的公有成员和保护成员在派生类中的访问属性不变。
(3) 在保护继承方式下,基类中的公有成员和保护成员在派生类中的访问属性均为保护的。
(4) 在私有继承方式下,基类中的公有成员和保护成员在派生类中的访问属性均为私有的。
保护成员与私有成员在同一个类中的用法完全一样。唯一的不同是,保护成员可被派生类直接访问,而私有成员在派生类中是不可访问的。
4. 派生类的构造函数和析构函数
基类的构造函数和析构函数不能被继承。在派生类中,应该加入新的构造函数以对派生类新增的成员进行初始化。派生类调用基类的构造函数对从基类继承来的成员进行初始化。
派生类的构造函数名与派生类名相同。当基类构造函数不带参数时,派生类不一定需要定义构造函数,然而当基类的构造函数哪怕只带有一个参数,它所有的派生类都必须定义构造函数。构造函数的参数表需要列出初始化基类数据成员、新增内嵌对象数据及新增一般数据成员所需要的全部参数。在定义派生类对象时构造函数的执行顺序是先祖先,再客人,后自己。所谓祖先是指基类,调用顺序按照它们继承时说明的顺序。客人指对象成员,调用顺序按照它们在类中说明的顺序。自己即派生类本身。
同样,在派生过程中,基类的析构函数不能被继承,派生类也需要加入新的析构函数。派生类与基类的析构函数没有什么联系,彼此独立,派生类或基类的析构函数只进行各自类对象销毁前的处理工作。派生类析构函数的定义方法与没有继承关系的类中析构函数的定义方法完全相同。在派生类析构函数里不必显示调用基类及成员对象的析构函数,系统会自动调用它们。析构函数的执行顺序和构造函数正好完全相反:先自己(派生类本身),再客人(对象成员),后祖先(基类)。
[例20.1] 基类成员在派生类中的访问权限(公有继承、私有继承、保护继承)演示。
[错误代码]
1 |
#include<iostream.h> |
2 |
#include<string.h> |
3 |
class Base |
4 |
{ |
5 |
private: |
6 |
void BasePrivate( ) |
7 |
{ |
8 |
cout<<"这是基类的私有成员函数。"<<endl; |
9 |
} |
10 |
protected: |
11 |
void BaseProtected( ) |
12 |
{ |
13 |
cout<<"这是基类的保护成员函数。"<<endl; |
14 |
} |
15 |
public: |
16 |
void BasePublic( ) |
17 |
{ |
18 |
cout<<"这是基类的公有成员函数。"<<endl; |
19 |
} |
20 |
}; |
21 |
class DeriveA : public Base |
22 |
{ |
23 |
}; |
24 |
class DeriveB : private Base |
25 |
{ |
26 |
}; |
27 |
class DeriveC : protected Base |
28 |
{ |
29 |
}; |
30 |
class DeriveD : public DeriveC |
31 |
{ |
32 |
}; |
33 |
|
34 |
void main( ) |
35 |
{ |
36 |
DeriveA da; |
37 |
DeriveB db; |
38 |
DeriveC dc; |
39 |
DeriveD dd; |
40 |
|
41 |
da.BasePrivate( ); |
42 |
da.BaseProtected( ); |
43 |
da.BasePublic( ); |
44 |
|
45 |
db.BasePrivate( ); |
46 |
db.BaseProtected( ); |
47 |
db.BasePublic( ); |
48 |
|
49 |
dc.BasePrivate( ); |
50 |
dc.BaseProtected( ); |
51 |
dc.BasePublic( ); |
52 |
|
53 |
dd.BasePrivate( ); |
54 |
dd.BaseProtected( ); |
55 |
dd.BasePublic( ); |
56 |
} |
[编译结果]
E:\Code\20_1.cpp(41)
: error C2248: 'BasePrivate' : cannot access private member declared in class
'Base'
E:\Code\20_1.cpp(6)
: see declaration of 'BasePrivate'
E:\Code\20_1.cpp(42)
: error C2248: 'BaseProtected' : cannot access protected member declared in
class 'Base'
E:\Code\20_1.cpp(11) : see declaration of 'BaseProtected'
E:\Code\20_1.cpp(45)
: error C2248: 'BasePrivate' : cannot access private member declared in class
'Base'
E:\Code\20_1.cpp(6) : see declaration of 'BasePrivate'
E:\Code\20_1.cpp(46)
: error C2248: 'BaseProtected' : cannot access protected member declared in
class 'Base'
E:\Code\20_1.cpp(11) : see
declaration of 'BaseProtected'
E:\Code\20_1.cpp(47)
: error C2248: 'BasePublic' : cannot access public member declared in class
'Base'
E:\Code\20_1.cpp(16) : see declaration of 'BasePublic'
E:\Code\20_1.cpp(49)
: error C2248: 'BasePrivate' : cannot access private member declared in class
'Base'
E:\Code\20_1.cpp(6) : see declaration of 'BasePrivate'
E:\Code\20_1.cpp(50)
: error C2248: 'BaseProtected' : cannot access protected member declared in
class 'Base'
E:\Code\20_1.cpp(11) : see
declaration of 'BaseProtected'
E:\Code\20_1.cpp(51)
: error C2248: 'BasePublic' : cannot access public member declared in class
'Base'
E:\Code\20_1.cpp(16) : see declaration of 'BasePublic'
E:\Code\20_1.cpp(53)
: error C2248: 'BasePrivate' : cannot access private member declared in class
'Base'
E:\Code\20_1.cpp(6) : see declaration of 'BasePrivate'
E:\Code\20_1.cpp(54)
: error C2248: 'BaseProtected' : cannot access protected member declared in
class 'Base'
E:\Code\20_1.cpp(11) : see
declaration of 'BaseProtected'
E:\Code\20_1.cpp(55)
: error C2248: 'BasePublic' : cannot access public member declared in class
'Base'
E:\Code\20_1.cpp(16) : see declaration of 'BasePublic'
[问题分析]
在本例中,Base是基类,DeriveA、DeriveB、DeriveC、DeriveD是派生类。DeriveA公有继承Base,DeriveB私有继承Base,DeriveC保护继承Base,DeriveD公有继承DeriveC。
表20.1基类成员在派生类中的访问权限
类名 |
继承方式 |
函数 |
特性 |
Base |
基类 |
BasePrivate BaseProtected BasePublic |
private protected public |
DeriveA |
公有继承 Base |
BasePrivate BaseProtected BasePublic |
不可访问 protected public |
DeriveB |
私有继承 Base |
BasePrivate BaseProtected BasePublic |
不可访问 Private private |
DeriveC |
保护继承 Base |
BasePrivate BaseProtected BasePublic |
不可访问 protected protected |
DeriveD |
公有继承 DeriveC |
BasePrivate BaseProtected BasePublic |
不可访问 protected protected |
由编译结果及表20.1可以看出,在4个派生类中,只有DeriveA类的对象可以访问BasePublic成员函数。其余的被继承的成员函数(除不可访问的以外),都是私有的或保护的,都只能通过自身的成员函数访问它们。
[正确代码]
把main函数里的代码去掉非法语句,改为:
34 |
void main( ) |
35 |
{ |
36 |
DeriveA da; |
37 |
da.BasePublic( ); |
38 |
} |
[运行结果]
这是基类的公有成员函数。
[例20.2] 由“交通工具”类派生出“小汽车”类和“卡车”类。
[错误代码]
1 |
#include "iostream.h" |
2 |
class Vehicle //“交通工具”类 |
3 |
{ |
4 |
public: |
5 |
Vehicle(int w) |
6 |
{ |
7 |
wheels = w; |
8 |
} |
9 |
|
10 |
int GetWheels( ) //得到轮子数量 |
11 |
{ |
12 |
return wheels; |
13 |
} |
14 |
private: |
15 |
int wheels; |
16 |
}; |
17 |
|
18 |
class Car : public Vehicle //“小汽车”类 |
19 |
{ |
20 |
public: |
21 |
Car( ) |
22 |
{ |
23 |
wheels = 4; |
24 |
} |
25 |
}; |
26 |
|
27 |
class Truck : public Vehicle //“卡车”类 |
28 |
{ |
29 |
public: |
30 |
Truck( ) |
31 |
{ |
32 |
wheels = 6; |
33 |
} |
34 |
}; |
35 |
|
36 |
int main(int argc, char* argv[]) |
37 |
{ |
38 |
Car car; |
39 |
cout<<"小汽车有"<<car.GetWheels( )<<"个轮子。"<<endl; |
40 |
|
41 |
Truck truck; |
42 |
cout<<"卡车有"<<truck.GetWheels( )<<"个轮子。"<<endl; |
43 |
|
44 |
return 0; |
45 |
} |
[编译结果]
E:\Code\20_2.cpp(21)
: error C2512: 'Vehicle' : no appropriate default constructor available
E:\Code\20_2.cpp(23)
: error C2248: 'wheels' : cannot access private member declared in class
'Vehicle'
[问题分析]
在本例中,Vehicle类中的wheels是私有类型的数据成员,派生类没有访问权限。应该由基类的构造函数初始化。即在Car的构造函数中调用Vehicle类的构造函数来初始化wheels成员。
[正确代码]
把Car类、Truck类的构造函数分别修改如下:
18 |
class Car : public Vehicle //“小汽车”类 |
19 |
{ |
20 |
public: |
21 |
Car( ) : Vehicle(4) |
22 |
{ |
23 |
} |
24 |
}; |
25 |
|
26 |
class Truck : public Vehicle //“卡车”类 |
27 |
{ |
28 |
public: |
29 |
Truck( ) :
Vehicle(6) |
30 |
{ |
31 |
} |
32 |
}; |
[运行结果]
小汽车有4个轮子。
卡车有6个轮子。
[例20.3] 多继承“床”类和“沙发”类,派生出“沙发床”类(多继承的多个基类有相同的成员函数)。
[错误代码]
1 |
#include<iostream.h> |
2 |
class Bed //“床”类 |
3 |
{ |
4 |
public: |
5 |
Bed( ) |
6 |
{ |
7 |
weight = 20; |
8 |
} |
9 |
void Sleep( ) |
10 |
{ |
11 |
cout <<"Sleeping...\n"; |
12 |
} |
13 |
void SetWeight(int i) |
14 |
{ |
15 |
weight =i; |
16 |
} |
17 |
protected: |
18 |
int weight; |
19 |
}; |
20 |
|
21 |
class Sofa //“沙发”类 |
22 |
{ |
23 |
public: |
24 |
Sofa( ) |
25 |
{ |
26 |
weight = 30; |
27 |
} |
28 |
void WatchTV( ) |
29 |
{ |
30 |
cout <<"Watching TV.\n"; |
31 |
} |
32 |
void SetWeight(int i) |
33 |
{ |
34 |
weight =i; |
35 |
} |
36 |
protected: |
37 |
int weight; |
38 |
}; |
39 |
|
40 |
class SofaBed :public Bed, public Sofa //“沙发床”类 |
41 |
{ |
42 |
public: |
43 |
void FoldOut( ) //“折叠”方法 |
44 |
{ |
45 |
cout <<"Fold out the sofa.\n"; |
46 |
} |
47 |
}; |
48 |
|
49 |
void main( ) |
50 |
{ |
51 |
SofaBed sb; |
52 |
sb.WatchTV( ); |
53 |
sb.FoldOut( ); |
54 |
sb.Sleep( ); |
55 |
sb.SetWeight(20); |
56 |
} |
[编译结果]
E:\Code\20_3.cpp(54)
: error C2385: 'SofaBed::SetWeight' is ambiguous
E:\Code\20_3.cpp(54)
: warning C4385: could be the 'SetWeight' in base 'Bed' of class 'SofaBed'
E:\Code\20_3.cpp(54)
: warning C4385: or the 'SetWeight' in base 'Sofa' of class 'SofaBed'
[问题分析]
当一个派生类是由多个基类派生而来时,假定这些基类中存在着名字相同的成员。如果使用一个表达式引用了这些同名的成员,就无法确定是引用哪个基类的成员,这种对基类成员的访问就是二义性的。要避免此种情况,可以使用成员名限定来消除二义性,也就是在成员名前用对象名及基类名来限定。
在本例中,“SofaBed”类是由多继承而来,其基类是“Sofa”类和“Bed”类。“Sofa”类和“Bed”类都有成员函数void SetWeight(int i),则“SofaBed”类的对象调用该成员函数时,如:
sb.SetWeight(20);
就产生了二义性。编译结果很清楚地告诉我们,无法确定调用的是哪一个'SetWeight'成员函数。解决办法是在SetWeight前用对象名及基类名来限定。
[正确代码]
把第55行代码改为:
55 |
sb.Bed::SetWeight(20); |
或
55 |
sb.Sofa::SetWeight(20); |
[运行结果]
Watching TV.
Fold out the sofa.
Sleeping...
[例20.4] 多继承的多个基类有共同的基类。
[错误代码]
1 |
#include<iostream.h> |
2 |
#include<string.h> |
3 |
class Base |
4 |
{ |
5 |
public: |
6 |
void BasePublic( ) |
7 |
{ |
8 |
cout<<"这是基类的公有成员函数。"<<endl; |
9 |
} |
10 |
}; |
11 |
|
12 |
class DeriveA : public Base |
13 |
{ |
14 |
}; |
15 |
|
16 |
class DeriveB : public Base |
17 |
{ |
18 |
}; |
19 |
|
20 |
class DeriveC : public DeriveA, public DeriveB |
21 |
{ |
22 |
}; |
23 |
|
24 |
void main( ) |
25 |
{ |
26 |
DeriveC dc; |
27 |
dc.BasePublic( ); |
28 |
} |
[编译结果]
E:\Code\20_4.cpp(27)
: error C2385: 'DeriveC::BasePublic' is ambiguous
E:\Code\20_4.cpp(27)
: warning C4385: could be the 'BasePublic' in base 'Base' of base 'DeriveA' of
class 'DeriveC'
E:\Code\20_4.cpp(27)
: warning C4385: or the 'BasePublic' in base 'Base' of base 'DeriveB' of class
'DeriveC'
[问题分析]
如果一个派生类从多个基类中派生,而这些基类又有一个共同的基类,则在这个派生类中访问这个共同基类中的成员时会产生二义性。要避免此种情况,可以利用虚基类。
在本例中,DeriveA和DeriveB都是基类Base的派生类,而DeriveA和DeriveB又派生出DeriveC。在DeriveC的对象访问从基类继承来的成员函数时,就会出现二义性。编译结果很清楚地告诉我们,无法确定调用的是哪一个'BasePublic'成员函数。
[正确代码]
在定义Base 的派生类DeriveA和DeriveB时,在public之前加上virtual关键字,则DeriveA和DeriveB被定义为虚基类。则在创建DeriveC时对公共基类的成员只进行一次初始化,消除了二义性。第12、16行代码改动如下:
12 |
class DeriveA : virtual public Base |
16 |
class DeriveB : virtual public Base |
[运行结果]
这是基类的公有成员函数。
[例20.5] 定义“家具”类,派生出“床”类(把一个派生类对象复制到一个基类对象)。
[错误代码]
1 |
#include<iostream.h> |
2 |
class Furniture // 定义“家具”类 |
3 |
{ |
4 |
public: |
5 |
Furniture( ) |
6 |
{ |
7 |
} |
8 |
void SetWeight(int i) |
9 |
{ |
10 |
weight =i; |
11 |
} |
12 |
int GetWeight( ) |
13 |
{ |
14 |
return weight; |
15 |
} |
16 |
protected: |
17 |
int weight; |
18 |
}; |
19 |
|
20 |
class Bed : public Furniture // 定义“床”类 |
21 |
{ |
22 |
public: |
23 |
int height; |
24 |
Bed( ) |
25 |
{ |
26 |
height=0; |
27 |
} |
28 |
void SetHeight(int h) |
29 |
{ |
30 |
height=h; |
31 |
} |
32 |
}; |
33 |
|
34 |
void main( ) |
35 |
{ |
36 |
Bed bed; |
37 |
bed.SetHeight(30); |
38 |
cout<<"床的高度为:"<<bed.height<<endl;; |
39 |
Furniture furniture = bed; |
40 |
Bed* pBed = (Bed*)(&furniture); |
41 |
cout<<"床的高度为:"<<pBed->height<<endl;; |
42 |
} |
[编译结果]
编译通过。
[问题分析]
编译通过,但运行结果与预期不符:
床的高度为:30
床的高度为:-858993460
派生类Bed的对象bed,被赋值给基类Furniture的对象furniture。在赋值后,派生类Bed专属的数据成员被切除,导致未预料的错误。对象bed的高度值30在赋值后丢失了。这是由于传值方式造成的。如果直接传递地址指针,就不会有问题。
[正确代码]
把第30、40两行代码改为:
39 |
Bed* pBed = &bed; |
[运行结果]
床的高度为:30
床的高度为:30
请改正如下程序中的错误。
1) 定义“数码设备”类,派生出“手机”类和“MP
1 |
#include<iostream.h> |
2 |
class CDigitalDevice |
3 |
{ |
4 |
private: |
5 |
int m_nElectricMax; //最大充电容量 |
6 |
int m_nElectricQuantity ; //当前电量 |
7 |
public: |
8 |
CDigitalDevice(int nElectronic, int nElectricMax) |
9 |
{ |
10 |
m_nElectricQuantity = nElectronic; |
11 |
m_nElectricMax = nElectricMax; |
12 |
} |
13 |
void ReChargeElectric(int n) //充电 |
14 |
{ |
15 |
m_nElectricQuantity = (m_nElectricQuantity+n > m_nElectricMax) ? |
16 |
m_nElectricMax : m_nElectricQuantity+n; |
17 |
} |
18 |
}; |
19 |
class CMobile : public CDigitalDevice |
20 |
{ |
21 |
public: |
22 |
float m_fMoney; //当前余额(元) |
23 |
CMobile(int nElectronic, int nElectricMax, float fMoney) |
24 |
{ |
25 |
m_nElectricQuantity = nElectronic; |
26 |
m_nElectricMax = nElectricMax; |
27 |
m_fMoney = fMoney; |
28 |
} |
29 |
void ReChargeMoney(float fMoney) //充值(fMoney:元) |
30 |
{ |
31 |
m_fMoney += fMoney; |
32 |
} |
33 |
void CallOut(int nTime) //每分钟通话耗电1, 且主叫收费两角 |
34 |
{ |
35 |
int time = nTime; |
36 |
if(m_fMoney<nTime*0.2) time = int(m_fMoney/0.2); |
37 |
if(m_nElectricQuantity<time) time = m_nElectricQuantity; |
38 |
|
39 |
m_nElectricQuantity -= time; |
40 |
m_fMoney -= float(time*0.2); |
41 |
} |
42 |
void CallIn(int nTime) //每分钟通话耗电1, 且被叫不收费 |
43 |
{ |
44 |
m_nElectricQuantity = (m_nElectricQuantity>nTime) ? m_nElectricQuantity-nTime : 0; |
45 |
} |
46 |
void PrintInfo( ) |
47 |
{ |
48 |
cout<<"电量:"<<m_nElectricQuantity<<endl; |
49 |
cout<<"余额:"<<m_fMoney<<"元"<<endl; |
50 |
} |
51 |
}; |
52 |
class CMp3 : public CDigitalDevice |
53 |
{ |
54 |
public: |
55 |
int m_nSongCount; //当前存储歌曲数,每首歌1MB |
56 |
int m_nMemorySize; //存储器容量(MB) |
57 |
|
58 |
CMp3(int nElectronic, int nElectronicCapacity, \ |
59 |
int nMemorySize, int nSongCount) |
60 |
{ |
61 |
m_nElectricQuantity = nElectronic; |
62 |
m_nElectricMax = nElectricMax; |
63 |
m_nSongCount = nSongCount; |
64 |
m_nMemorySize = nMemorySize; |
65 |
} |
66 |
void PrintInfo( ) |
67 |
{ |
68 |
cout<<"电量:"<<m_nElectricQuantity<<endl; |
69 |
cout<<"存储空间:"<<m_nMemorySize<<"MB"<<endl; |
70 |
cout<<"歌曲数:"<<m_nSongCount<<"首"<<endl; |
71 |
} |
72 |
}; |
73 |
void main( ) |
74 |
{ |
75 |
cout<<"手机:"<<endl; |
76 |
CMobile mobile(10,100,10); |
77 |
mobile.ReChargeElectric(10); |
78 |
mobile.CallIn(2); |
79 |
mobile.CallOut(5); |
80 |
mobile.PrintInfo( ); |
81 |
|
82 |
cout<<endl; |
83 |
cout<<"MP3:"<<endl; |
84 |
CMp3 mp3(10,50,256,0); |
85 |
mp3.ReChargeElectric(10); |
86 |
mp3.PrintInfo( ); |
87 |
} |
2) 定义“房子”类和“卡车”类,派生出“房车”类。
1 |
#include "iostream.h" |
2 |
class House //“房子”类 |
3 |
{ |
4 |
protected: |
5 |
float area; //面积 |
6 |
int bedCount; //床数 |
7 |
public: |
8 |
House(float a,int bc): |
9 |
area(a),bedCount(bc) |
10 |
{ |
11 |
} |
12 |
void Clean( ) |
13 |
{ |
14 |
cout<<"正在打扫房间......"<<endl; |
15 |
cout<<"房间打扫完毕!"<<endl; |
16 |
} |
17 |
}; |
18 |
class Truck //“卡车”类 |
19 |
{ |
20 |
protected: |
21 |
float area; //面积 |
22 |
float speed; //速度 |
23 |
int seatCount; //座位数 |
24 |
public: |
25 |
Truck(float a, float s, int sc) |
26 |
: area(a),speed(s),seatCount(sc) |
27 |
{ |
28 |
} |
29 |
void Go( ) |
30 |
{ |
31 |
cout<<"出发!"<<endl; |
32 |
cout<<"速度为每小时"<<speed<<"公里。"<<endl; |
33 |
cout<<"到达!"<<endl; |
34 |
} |
35 |
|
36 |
}; |
37 |
class MotorHome : public House, public Truck //“房车”类 |
38 |
{ |
39 |
protected: |
40 |
float price; //价格 |
41 |
public: |
42 |
MotorHome(float a,int bc,float s, int sc, float p) |
43 |
: House(a,bc),Truck(a,s,sc) |
44 |
{ |
45 |
price = p; |
46 |
} |
47 |
void ShowInfo( ) |
48 |
{ |
49 |
cout<<"房车信息"<<endl; |
50 |
cout<<"面积:"<<area<<"平方米"<<endl; |
51 |
cout<<"床数:"<<bedCount<<endl; |
52 |
cout<<"速度:"<<speed<<"公里/小时"<<endl; |
53 |
cout<<"座位数:"<<seatCount<<endl; |
54 |
cout<<"价格:"<<price<<"万元"<<endl; |
55 |
} |
56 |
}; |
57 |
void main( ) |
58 |
{ |
59 |
MotorHome mh(30,4,60,2,120); |
60 |
mh.ShowInfo( ); |
61 |
} |
1.运算符重载
(1)概述
C++中预定义的运算符的操作对象只能是基本数据类型。运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。当C++语言原有的一个运算符被重载之后,它原先所具有的语义并没有消失,只相当于针对一个特定的类定义了一个新的运算符。C++中的运算符几乎全部可以重载,但以下几个除外:
成员访问运算符“.”
作用域运算符“∷”
条件运算符“? :”
成员指针运算符“*”
编译预处理命令的开始符号“#”
运算符重载必须遵循一定的规则。程序员不能定义新的运算符,只能重载已有的运算符。 重载之后运算符的优先级和结合性都不能改变。运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造。一般来讲,重载的功能应当与原有功能相类似,不能改变原运算符所需操作数的个数,同时至少要有一个操作数是自定义类型。运算符重载可以使用成员函数和友元函数两种形式。运算符重载的实质就是函数重载。
关于赋值运算符的重载,在正常的情况下,系统会为每一个类自动生成一个默认的赋值运算符。但是,这种赋值只限于同一个类的对象之间赋值,而且相当于浅拷贝构造函数。如果需要在赋值时重新分配内存空间,需要自行重载赋值运算符。
(2)用成员函数重载运算符
只能使用成员函数重载的运算符有:=、(
)、[]、->、new、delete。单目运算符最好重载为成员函数。对于复合的赋值运算符如+=、-=、*=、/=、&=、!=、~=、%=、>>=、<<=建议重载为成员函数。用成员函数重载运算符的格式为:
type operator @ (〈参数表〉)
如果该运算符是一元的,则参数表为空,此时当前对象作为此运算符的单操作数;如果该运算符是二元的,则参数表中有一个操作数,此时当前对象作为此运算符的左操作数,参数表中的操作数作为此运算符的右操作数,以此类推。
(3)用友元函数重载运算符
除了上述运算符以外,其它运算符建议重载为友元函数。用友元函数重载运算符的格式为:
friend type operator @(〈参数表〉);
友元函数不属于任何类,没有this指针。因此用友元函数重载运算符时,所有的操作数均需要用参数来传递。若运算符是一元的,则参数表中有一个操作数;若运算符是二元的,则参数表中有两个操作数,以此类推。
2.虚函数
指向基类对象的指针都可以指向它的公有派生类对象。反之,不能将一个声明为指向派生类对象的指针指向其基类的一个对象。声明为指向基类对象的指针,当它指向公有派生类对象时,只能利用它来直接访问派生类中从基类继承来的成员,不能直接访问公有派生类中的其它成员。若想访问其公有派生类的特定成员,可以将基类指针显式地转换为派生类指针来实现。
虚函数提供了一种接口界面,用来表现基类和派生类的成员函数之间的一种关系。虚函数在基类中定义。在基类中的某个成员函数被声明为虚函数后,该函数可以在派生类中被重新定义。重新定义虚函数时,必须与基类中的原型完全相同。虚函数是一种非静态的成员函数,说明格式如下:
virtual 〈类型〉〈函数名〉(〈参数表〉)
;
使用虚函数时应注意以下几点:
在派生类中重新定义基类中的虚函数时,可以不加virtual,但函数原型必须与基类中的完全相同。
析构函数可以是虚函数,但构造函数不能是虚函数。一般地讲,若某类中定义有虚函数,则其析构函数也应当说明为虚函数。
访问一个虚函数时,可以使用指向基类的指针或对基类的引用,以满足运行时多态要求。
若在派生类中没有重新定义虚函数,则派生类的对象将使用其基类中的虚函数代码。
虚函数必须是类的成员函数,不能是友元,但它可以是另一个类的友元。
虚函数不能是静态成员函数。
使用虚函数后,不得使用类作用域区分符强制指明要引用的虚函数。
若在基类的构造函数或析构函数中也引用虚函数,则所引用的只能是本类的虚函数。因为此时派生类中的构造函数或析构函数尚未执行完毕。
3.纯虚函数
如果基类只表达一些抽象的概念,而并不与具体的对象相联系,但又希望为派生类提供一个公共的界面,在这种情况下,可以将基类中的虚函数定义成纯虚函数。纯虚函数是一种没有具体实现的特殊的虚函数。纯虚函数的实现由其具体的派生类来提供。纯虚函数的定义格式如下:
virtual 〈类型〉〈函数名〉(〈参数表〉)
= 0;
纯虚函数所在类的构造函数和析构函数中不允许调用纯虚函数,但其它成员函数可以调用。
4.抽象类
如果一个类中至少有一个纯虚函数,那么该类被称为抽象类。
抽象类只能作为基类来派生新类,不能建立抽象类的对象,也不能用作参数类型、函数返回值类型、或显式转换的类型。可以声明指向抽象类的指针和引用,并使这些指针指向它的公有派生类对象,进而实现多态性。如果基类是抽象类,而派生类并没有重新定义这些纯虚函数的覆盖成员函数,那么该派生类也是抽象类。因此,从一个抽象类派生的具体类必须提供所有纯虚函数的实现代码。
5.虚析构函数
抽象类的析构函数可以被声明为纯虚函数。由于派生类的析构函数不可能和抽象类的析构函数同名,因此,提供一个默认的析构函数的实现是完全必要的。一般情况下,抽象类的析构函数是在释放派生类对象时由派生类的析构函数隐含调用的。当指向基类的指针指向新建立的派生类对象而且基类和派生类都调用new向堆申请空间时,必须将基类的析构函数声明为虚函数,从而派生类的析构函数也为虚函数,这样才能在程序结束时自动地调用它,从而将派生类对象申请的空间退还给堆。
[例21.1] 利用运算符重载实现两个字符串的连接。
[错误代码]
1 |
#include<iostream.h> |
2 |
#include<string.h> |
3 |
|
4 |
class MyString |
5 |
{ |
6 |
protected: |
7 |
char str[255]; |
8 |
public: |
9 |
MyString(char* s) |
10 |
{ |
11 |
strcpy(str,s); |
12 |
} |
13 |
|
14 |
void StringCat(char* s) //连接两个字符串 |
15 |
{ |
16 |
strcat(str,s); |
17 |
} |
18 |
|
19 |
char* GetString( ) |
20 |
{ |
21 |
return str; |
22 |
} |
23 |
|
24 |
MyString operator @(MyString& ms) |
25 |
{ |
26 |
MyString tmp(str); |
27 |
tmp.StringCat(ms.str); |
28 |
return tmp; |
29 |
} |
30 |
}; |
31 |
|
32 |
void main( ) |
33 |
{ |
34 |
MyString ms1("Hello,"); |
35 |
MyString ms2("world!"); |
36 |
MyString ms3 = ms1 @ ms2; |
37 |
cout<<ms3.GetString( )<<endl; |
38 |
} |
[编译结果]
E:\Code\21_1.cpp(24)
: error C2018: unknown character '0x40'
E:\\Code\21_1.cpp(24)
: error C2544: expected ')' for operator '( )'
E:\\Code\21_1.cpp(24)
: error C2146: syntax error : missing ';' before identifier 'MyString'
E:\\Code\21_1.cpp(24)
: error C2460: '( )' : uses 'MyString', which is being defined
E:\\Code\21_1.cpp(5) : see declaration of 'MyString'
E:\\Code\21_1.cpp(24)
: error C2059: syntax error : ')'
E:\\Code\21_1.cpp(26)
: error C2061: syntax error : identifier 'str'
E:\\Code\21_1.cpp(27)
: error C2143: syntax error : missing ';' before '.'
E:\\Code\21_1.cpp(27)
: error C2501: 'tmp' : missing storage-class or type specifiers
E:\\Code\21_1.cpp(27)
: error C2059: syntax error : '.'
E:\\Code\21_1.cpp(27)
: error C2238: unexpected token(s) preceding ';'
E:\\Code\21_1.cpp(28)
: error C2059: syntax error : 'return'
E:\\Code\21_1.cpp(28)
: error C2238: unexpected token(s) preceding ';'
E:\\Code\21_1.cpp(30)
: error C2143: syntax error : missing ';' before '}'
E:\\Code\21_1.cpp(30)
: error C2143: syntax error : missing ';' before '}'
E:\\Code\21_1.cpp(30)
: error C2143: syntax error : missing ';' before '}'
E:\\Code\21_1.cpp(30)
: error C2143: syntax error : missing ';' before '}'
E:\\Code\21_1.cpp(36)
: error C2018: unknown character '0x40'
E:\\Code\21_1.cpp(36)
: error C2146: syntax error : missing ';' before identifier 'ms2'
[问题分析]
在本例中,程序员的意图是采用运算符重载的方式,把两个MyString类的对象连接起来。问题是试图重载的符号“@”并不是运算符。运算符重载时不可臆造新的运算符。可采用合法的运算符,如“+”。编译器给出许多莫名其妙的错误信息,其实都是这个 “@” 符号所致。
[正确代码]
修改的代码如下:
24 |
MyString operator +(MyString& ms) |
36 |
MyString ms3 = ms1 + ms2; |
[运行结果]
Hello,world!
[例21.2] 定义Time类的时候重载运算符“<<”,以“时分秒”格式输出。
[错误代码]
1 |
#include<iostream.h> |
2 |
class Time |
3 |
{ |
4 |
private: |
5 |
int hour; //时 |
6 |
int minute; //分 |
7 |
int second; //秒 |
8 |
public: |
9 |
Time(int h,int m,int s) |
10 |
{ |
11 |
hour = h; |
12 |
minute = m; |
13 |
second = s; |
14 |
} |
15 |
friend ostream& operator<<(ostream& stream, Time& t, Time& t2); |
16 |
}; |
17 |
|
18 |
ostream& operator<<(ostream& stream, Time& t, Time& t2) |
19 |
{ |
20 |
cout<<t.hour<<"点"<<t.minute<<"分"<<t.second<<"秒"<<endl; |
21 |
return stream; |
22 |
} |
23 |
|
24 |
void main( ) |
25 |
{ |
26 |
Time t(9,10,30); |
27 |
cout<<t<<endl; |
28 |
} |
[编译结果]
E:\Code\21_2.cpp(15)
: error C2804: binary 'operator <<' has too many parameters
E:\Code\21_2.cpp(19)
: error C2804: binary 'operator <<' has too many parameters
E:\Code\21_2.cpp(20)
: error C2248: 'hour' : cannot access private member declared in class 'Time'
E:\Code\21_2.cpp(5) : see
declaration of 'hour'
E:\Code\21_2.cpp(20)
: error C2248: 'minute' : cannot access private member declared in class 'Time'
E:\Code\21_2.cpp(6) : see declaration of 'minute'
E:\Code\21_2.cpp(20)
: error C2248: 'second' : cannot access private member declared in class 'Time'
E:\Code\21_2.cpp(7) : see declaration of 'second'
E:\Code\21_2.cpp(28)
: error C2679: binary '<<' : no operator defined which takes a right-hand
operand of type 'class Time' (or there is no acceptable conversion)
[问题分析]
编译器给出错误信息,认为重载运算符“<<”时参数太多了。运算符重载时不能改变运算符操作数的个数。去掉多余的参数即可。
[正确代码]
改动的代码如下:
15 |
friend ostream& operator<<(ostream& stream, Time& t); |
18 |
ostream& operator<<(ostream& stream, Time& t) |
[运行结果]
9点10分30秒
[例21.3] 重载自增运算符。
[错误代码]
1 |
#include <iostream.h> |
2 |
class Increase |
3 |
{ |
4 |
public: |
5 |
Increase(int x):value(x){} |
6 |
Increase & operator++( ); // 前自增 |
7 |
void display( ){ cout <<"the value is " <<value <<endl; } |
8 |
private: |
9 |
int value; |
10 |
}; |
11 |
|
12 |
Increase & Increase::operator++( ) |
13 |
{ |
14 |
value++; // 先增量 |
15 |
return *this; // 再返回原对象 |
16 |
} |
17 |
|
18 |
void main( ) |
19 |
{ |
20 |
Increase n(20); |
21 |
n.display( ); |
22 |
(n++).display( ); // 显示临时对象值 |
23 |
n.display( ); // 显示原有对象 |
24 |
(++n).display( ); |
25 |
} |
[编译结果]
E:\Code\21_3.cpp(22)
: warning C4620: no postfix form of 'operator ++'
found for type 'Increase', using prefix form
E:\Code\21_3.cpp(3) : see declaration of 'Increase'
[问题分析]
自增、自减运算符的重载分别包括前缀运算和后缀运算。为了区别二者,将后缀运算符视为双目运算符。表达式:
Obj++ 或
Obj--
被看作:
Obj++0 或
Obj--0
本例中,程序员只重载了前缀运算的++运算符,因此编译器给出错误信息:“no postfix form of 'operator ++' found for type 'Increase', using
prefix form”。
[正确代码]
在第7行处增加:
7 |
Increase operator++(int); // 后自增 |
在第18行处增加:
18 |
Increase Increase::operator++(int) |
19 |
{ |
20 |
Increase temp(*this);
// 临时对象存放原有对象值 |
21 |
value++;
// 原有对象增量修改 |
22 |
return temp;
// 返回原有对象值 |
23 |
} |
[运行结果]
the value
is 20
the value
is 20
the value
is 21
the value
is 22
[例21.4] 试一试能否重载“?”运算符。
[错误代码]
1 |
#include <iostream.h> |
2 |
class MyClass |
3 |
{ |
4 |
public: |
5 |
char* operator ?( ) |
6 |
{ |
7 |
renturn "this is a class." |
8 |
} |
9 |
}; |
10 |
|
11 |
void main( ) |
12 |
{ |
13 |
} |
[编译结果]
E:\Code\21_4.cpp(6)
: error C2800: 'operator ?' cannot be overloaded
E:\Code\21_4.cpp(6)
: error C2333: '' : error in function declaration; skipping function body
[问题分析]
编译器很清楚地告诉我们:运算符“?”不能被重载。实际上,以下运算符都不能被重载:
(1) 成员访问运算符“.”
(2) 作用域运算符“∷”
(3) 条件运算符“? :”
(4) 成员指针运算符“*”
(5) 编译预处理命令的开始符号“#”
[例21.5] 定义一个“人民币”类,以友元方式重载“+”运算符。
[错误代码]
1 |
#include<iostream.h> |
2 |
class RMB //“人民币”类 |
3 |
{ |
4 |
public: |
5 |
RMB(unsigned int y, unsigned int j, unsigned int f); |
6 |
friend RMB operator +(RMB&); |
7 |
void display( ) |
8 |
{ |
9 |
cout <<yuan<<"元"<<jiao<<"角"<<fen<<"分"<<endl; |
10 |
} |
11 |
protected: |
12 |
unsigned int yuan; |
13 |
unsigned int jiao; |
14 |
unsigned int fen; |
15 |
}; |
16 |
|
17 |
RMB::RMB(unsigned int y, unsigned int j, unsigned int f) |
18 |
{ |
19 |
yuan = y; |
20 |
jiao = j; |
21 |
fen = f; |
22 |
while ( fen >=10 ) // 确保分值小于10 |
23 |
{ |
24 |
jiao ++; |
25 |
fen -= 10; |
26 |
} |
27 |
while ( jiao >=10 ) // 确保角值小于10 |
28 |
{ |
29 |
yuan ++; |
30 |
jiao -= 10; |
31 |
} |
32 |
} |
33 |
|
34 |
RMB operator+(RMB& s) // 定义友元运算符函数 |
35 |
{ |
36 |
unsigned int y = yuan + s.yuan; |
37 |
unsigned int j = jiao + s.jiao; |
38 |
unsigned int f = fen + s.fen; |
39 |
RMB tmp(y, j ,f); |
40 |
return tmp; |
41 |
} |
42 |
|
43 |
void main( ) |
44 |
{ |
45 |
RMB d1(1, 6, 4); |
46 |
RMB d2(2, 5, 7); |
47 |
RMB d3(0, 0, 0); |
48 |
d3 = d1 + d2; |
49 |
d3.display( ); |
50 |
} |
[编译结果]
E:\Code\21_5.cpp(36)
: error C2065: 'yuan' : undeclared identifier
E:\Code\21_5.cpp(37)
: error C2065: 'jiao' : undeclared identifier
E:\Code\21_5.cpp(38)
: error C2065: 'fen' : undeclared identifier
E:\Code\21_5.cpp(48)
: error C2676: binary '+' : 'class RMB' does not define this operator or a
conversion to a type acceptable to the predefined operator
[问题分析]
注意友元函数不属于任何类,它没有this指针,这与成员函数完全不同。若运算符是一元的,则参数表中有一个操作数;若运算符是二元的,则参数表中有两个操作数。也就是说在用友元定义时,所有的操作数均需要用参数来传递。友元运算符函数与成员运算符函数的主要区别在其参数个数不同。 “+”运算符是二元的,如果用成员函数进行运算符重载,需要1个参数;如果用友元函数进行运算符重载,需要2个参数。本例中,用友元函数进行运算符重载,少了一个参数。
[正确代码]
修改如下:
6 |
friend RMB operator +(RMB&, RMB&); |
… ….
34 |
RMB operator+(RMB& s1, RMB& s2) // 定义友元运算符函数 |
35 |
{ |
36 |
unsigned int y = s1.yuan + s2.yuan; |
37 |
unsigned int j = s1.jiao + s2.jiao; |
38 |
unsigned int f = s1.fen + s2.fen; |
39 |
RMB tmp(y, j ,f); |
40 |
return tmp; |
41 |
} |
[运行结果]
4元2角1分
[例21.6] 定义“分数”类,重载“=”运算符。
[错误代码]
1 |
#include <iostream.h> |
2 |
|
3 |
class Fraction //分数类 |
4 |
{ |
5 |
public: |
6 |
Fraction(int n, int d) |
7 |
{ |
8 |
numerator = n; |
9 |
denominator = d; |
10 |
} |
11 |
void Print( ) |
12 |
{ |
13 |
cout<<" "<<numerator<<endl; |
14 |
cout<<"---"<<endl; |
15 |
cout<<" "<<denominator<<endl; |
16 |
} |
17 |
friend Fraction& operator=(Fraction& f,Fraction& g); |
18 |
private: |
19 |
int numerator; //分子 |
20 |
int denominator; //分母 |
21 |
}; |
22 |
|
23 |
Fraction& operator=(Fraction& f,Fraction& g) |
24 |
{ |
25 |
f.numerator = g.numerator; |
26 |
f.denominator = g.denominator; |
27 |
return f; |
28 |
} |
29 |
|
30 |
void main( ) |
31 |
{ |
32 |
Fraction f1(1,3); |
33 |
Fraction f2 = f1; |
34 |
f2.Print( ); |
35 |
} |
[编译结果]
E:\Code\21_6.cpp(17)
: error C2801: 'operator =' must be a <Unknown> member
E:\Code\21_6.cpp(24)
: error C2801: 'operator =' must be a <Unknown> member
E:\Code\21_6.cpp(25)
: error C2248: 'numerator' : cannot access private member declared in class
'Fraction'
E:\Code\21_6.cpp(19) : see declaration of 'numerator'
E:\Code\21_6.cpp(25)
: error C2248: 'numerator' : cannot access private member declared in class
'Fraction'
E:\Code\21_6.cpp(19) : see declaration of 'numerator'
E:\Code\21_6.cpp(26)
: error C2248: 'denominator' : cannot access private member declared in class
'Fraction'
E:\Code\21_6.cpp(20) : see declaration of 'denominator'
E:\Code\21_6.cpp(26)
: error C2248: 'denominator' : cannot access private member declared in class
'Fraction'
E:\Code\21_6.cpp(20) : see declaration of 'denominator'
[问题分析]
有些运算符不能重载为友元函数,只能使用成员函数重载,这些运算符是:=、(
)、[]、->、new、delete。“=”如果被重载为友元函数,将会出现与赋值语义不一致的地方。
[正确代码]
把重载方式改为成员函数:
17 |
Fraction& operator=(Fraction& f); |
该成员函数的实现部分:
23 |
Fraction& Fraction::operator=(Fraction& f) |
24 |
{ |
25 |
numerator = f.numerator; |
26 |
denominator =
f.denominator; |
27 |
return *this; |
28 |
} |
其他代码不变。
[运行结果]
[例21.7] 定义“复数”类,重载“+”、“-”、“*”、“/”运算符。
[错误代码]
1 |
#include<iostream.h> |
2 |
class complex |
3 |
{ |
4 |
float real,imag; // 复数的实部和虚部 |
5 |
public: |
6 |
complex(float r=0,float i=0) |
7 |
{ |
8 |
real=r; |
9 |
imag=i; |
10 |
} |
11 |
void print( ); |
12 |
complex operator +(complex a); |
13 |
complex operator -(complex a); |
14 |
complex operator *(complex a); |
15 |
complex operator /(complex a); |
16 |
}; |
17 |
|
18 |
void complex::print( ) |
19 |
{ |
20 |
cout<<real; |
21 |
if(imag>0) cout<<"+"; // image小于0,则自带 “-” |
22 |
if(imag!=0) cout<<imag<<"i\n"; |
23 |
} |
24 |
|
25 |
complex complex::operator+(complex a ) |
26 |
{ |
27 |
complex temp; |
28 |
temp.real=a.real+ real; |
29 |
temp.imag=a.imag+ imag; |
30 |
return temp; |
31 |
} |
32 |
|
33 |
complex complex::operator-(complex a ) |
34 |
{ |
35 |
complex temp; |
36 |
temp.real=a.real- real; |
37 |
temp.imag=a.imag- imag; |
38 |
return temp; |
39 |
} |
40 |
|
41 |
complex complex::operator*(complex a ) |
42 |
{ |
43 |
complex temp; |
44 |
temp.real=a.real* real-a.imag* imag; |
45 |
temp.imag=a.real* imag+a.imag* real; |
46 |
return temp; |
47 |
} |
48 |
|
49 |
complex complex::operator/(complex a ) |
50 |
{ |
51 |
complex temp; |
52 |
float tt; |
53 |
tt=1/( real* real+ imag* imag); |
54 |
temp.real=(a.real* real+a.imag* imag)*tt; |
55 |
temp.imag=( real*a.imag-a.real* imag)*tt; |
56 |
return temp; |
57 |
} |
58 |
|
59 |
void main( ) |
60 |
{ |
61 |
complex c1( |
62 |
complex c2 = 3 + c1; |
63 |
c1.print( ); |
64 |
c2.print( ); |
65 |
} |
[编译结果]
E:\Code\21_7.cpp(62)
: error C2677: binary '+' : no global operator defined which takes type 'class
complex' (or there is no acceptable conversion)
[问题分析]
当运算符的左操作数是一个常数时,就不能利用成员函数重载运算符,应当用友元函数重载。当利用成员函数重载运算符时,3 + c1被解释为3.
operator+(c1),显然是错误的;而当利用友元函数重载运算符时,3
+ c1被解释为operator+(complex(3),
c1) ,显然是合法的。所以,对双目运算符还是重载为友元函数比重载为成员函数更方便些,除了不能重载为友元函数的运算符,如:=、(
)、[]、->、new、delete。
[正确代码]
把程序改为利用友元函数重载运算符。
12 |
friend complex operator +(complex a,complex b); |
13 |
friend complex operator -(complex a,complex b); |
14 |
friend complex operator *(complex a,complex b); |
15 |
friend complex operator /(complex a,complex b); |
它们的实现代码:
25 |
complex operator+(complex a,complex b) |
26 |
{ |
27 |
complex temp; |
28 |
temp.real=a.real+b.real; |
29 |
temp.imag=a.imag+b.imag; |
30 |
return temp; |
31 |
} |
32 |
|
33 |
complex operator-(complex a,complex b) |
34 |
{ |
35 |
complex temp; |
36 |
temp.real=a.real-b.real; |
37 |
temp.imag=a.imag-b.imag; |
38 |
return temp; |
39 |
} |
40 |
|
41 |
complex operator*(complex a,complex b) |
42 |
{ |
43 |
complex temp; |
44 |
temp.real=a.real*b.real-a.imag*b.imag; |
45 |
temp.imag=a.real*b.imag+a.imag*b.real; |
46 |
return temp; |
47 |
} |
48 |
|
49 |
complex operator/(complex a,complex b) |
50 |
{ |
51 |
complex temp; |
52 |
float tt; |
53 |
tt=1/(b.real*b.real+b.imag*b.imag); |
54 |
temp.real=(a.real*b.real+a.imag*b.imag)*tt; |
55 |
temp.imag=(b.real*a.imag-a.real*b.imag)*tt; |
56 |
return temp; |
57 |
} |
[运行结果]
2.3+4.6i
5.3+4.6i
[例21.8] 由“儿童”类派生出“男孩”类和“女孩”类,实现虚函数GetHobby(返回爱好)。
[错误代码]
1 |
#include "iostream.h" |
2 |
class Child //“儿童”类 |
3 |
{ |
4 |
public: |
5 |
virtual static void GetHobby( ) //返回爱好 |
6 |
{ |
7 |
} |
8 |
}; |
9 |
|
10 |
class Boy : public Child //“男孩”类 |
11 |
{ |
12 |
public: |
13 |
void GetHobby( ) //返回爱好 |
14 |
{ |
15 |
cout<<"男孩喜欢打球!"<<endl; |
16 |
} |
17 |
}; |
18 |
|
19 |
class Girl : public Child //“女孩”类 |
20 |
{ |
21 |
public: |
22 |
void GetHobby( ) //返回爱好 |
23 |
{ |
24 |
cout<<"女孩喜欢逛街!"<<endl; |
25 |
} |
26 |
}; |
27 |
|
28 |
int main(int argc, char* argv[]) |
29 |
{ |
30 |
Boy boy; |
31 |
boy.GetHobby( ); |
32 |
|
33 |
Girl girl; |
34 |
girl.GetHobby( ); |
35 |
|
36 |
return 0; |
37 |
} |
[编译结果]
E:\Code\21_8.cpp(6)
: error C2576: 'GetHobby' : virtual used for static member function
[问题分析]
虚函数不能是一个静态成员函数。第5行
virtual static void GetHobby( )
中,把成员函数GetHobby声明为静态成员函数是错误的。编译结果明确指出了这一点。
[正确代码]
修改如下:
5 |
virtual void GetHobby( ) |
[运行结果]
男孩喜欢打球!
女孩喜欢逛街!
[例21.9] 由“动物”类派生出“老虎”类和“绵羊”类,并实现虚函数soar。
[错误代码]
1 |
#include "iostream.h" |
2 |
class Animal //“动物”类 |
3 |
{ |
4 |
public: |
5 |
virtual friend void soar( ) { } //叫 |
6 |
}; |
7 |
|
8 |
class Tiger : public Animal //“老虎”类 |
9 |
{ |
10 |
public: |
11 |
void soar( ); |
12 |
}; |
13 |
|
14 |
void Tiger::soar( ) |
15 |
{ |
16 |
cout<<"Ooooooo!"<<endl; |
17 |
} |
18 |
|
19 |
class Sheep : public Animal //“绵羊”类 |
20 |
{ |
21 |
public: |
22 |
void soar( ); |
23 |
}; |
24 |
|
25 |
void Sheep::soar( ) |
26 |
{ |
27 |
cout<<"Mie mie!"<<endl; |
28 |
} |
29 |
|
30 |
int main(int argc, char* argv[]) |
31 |
{ |
32 |
Tiger tiger; |
33 |
tiger.soar( ); |
34 |
|
35 |
Sheep sheep; |
36 |
sheep.soar( ); |
37 |
|
38 |
return 0; |
39 |
} |
[编译结果]
E:\Code\21_9.cpp(5)
: error C2575: 'soar' : only member functions and bases can be virtual
[问题分析]
虚函数必须是类的一个成员函数,不能是友元函数,但它可以是另一个类的友元。第5行代码
virtual friend void soar( ) { }
中,把友元函数soar声明为虚函数是错误的。编译结果明确指出了这一点。
[正确代码]
修改如下:
5 |
virtual void soar( ) { } |
[运行结果]
Ooooooo!
Mie mie!
[例21.10] 定义“交通工具”类。
[错误代码]
1 |
#include "iostream.h" |
2 |
class Vehicle //“交通工具”类 |
3 |
{ |
4 |
public: |
5 |
virtual Vehicle( ) |
6 |
{ |
7 |
} |
8 |
}; |
9 |
|
10 |
int main(int argc, char* argv[]) |
11 |
{ |
12 |
return 0; |
13 |
} |
[编译结果]
E:\Code\21_10.cpp(5)
: error C2633: 'Vehicle' : 'inline' is the only legal storage class for
constructors
[问题分析]
构造函数不能是虚函数,但析构函数则可以是虚函数。一般地讲,若某类中定义有虚函数,则其析构函数也应当说明为虚函数。特别是在析构函数需要完成一些有意义的操作,例如释放内存时,尤其应当如此。
[例21.11] 将基类析构函数声明为非虚函数。
[错误代码]
1 |
#include <iostream.h> |
2 |
#include <string.h> |
3 |
class base |
4 |
{ |
5 |
char *p; |
6 |
public: |
7 |
base(int sz, char *bptr) |
8 |
{ |
9 |
p = new char [sz]; |
10 |
strcpy( p,bptr); |
11 |
cout<<"constructor base"<<endl; |
12 |
} |
13 |
|
14 |
~base( ) |
15 |
{ |
16 |
delete []p; |
17 |
cout << "destructor base\n"; |
18 |
} |
19 |
}; |
20 |
|
21 |
class derive: public base |
22 |
{ |
23 |
char *pp; |
24 |
public: |
25 |
derive(int sz1, int sz2, char *bp, char *dptr) : base(sz1, bp) |
26 |
{ |
27 |
pp = new char [sz2]; |
28 |
strcpy( pp, dptr); |
29 |
cout<<"constructor derive"<<endl; |
30 |
} |
31 |
|
32 |
~derive( ) |
33 |
{ |
34 |
delete []pp; |
35 |
cout << "destructor derive\n"; |
36 |
} |
37 |
}; |
38 |
|
39 |
void main( ) |
40 |
{ |
41 |
base *px = new derive(5 ,7 , "base", "derive"); //指向基类的指针可以指向公有派生类的对象 |
42 |
delete px; //执行delete自动调用析构函数 |
43 |
} |
[编译结果]
编译通过。
[问题分析]
一般情况下,抽象类的析构函数是在释放派生类对象时由派生类的析构函数隐含调用的。说明虚析构函数的作用在于使用delete运算符删除一个对象时,由于采取动态联编方式选择析构函数,确保释放对象较为彻底。
当指向基类的指针指向新建立的派生类对象而且基类和派生类都调用new向堆申请空间时,必须将基类的析构函数声明为虚函数,从而派生类的析构函数也为虚函数,这样才能在程序结束时自动地调用它,从而将派生类对象申请的空间退还给堆。
上面的程序里,基类析构函数声明为非虚函数,其运行结果如下:
constructor
base
constructor
derive
destructor
base
可见,没有执行派生类的析构函数,导致派生类的构造函数里分配的资源没有被释放。
[正确代码]
只需要把基类析构函数声明为虚函数:
14 |
virtual ~base( ) |
[运行结果]
constructor base
constructor derive
destructor derive
destructor base
[例21.12] 在构造函数里调用虚函数。
[错误代码]
1 |
#include <iostream.h> |
2 |
class Base |
3 |
{ |
4 |
public: |
5 |
Base( ) { } |
6 |
virtual void vfunc( ) |
7 |
{ |
8 |
cout<<"Base::vfunc( ) called"<<endl; |
9 |
} |
10 |
}; |
11 |
|
12 |
class DeriveA : public Base |
13 |
{ |
14 |
public: |
15 |
DeriveA( ) |
16 |
{ |
17 |
vfunc( ); |
18 |
} |
19 |
}; |
20 |
|
21 |
class DeriveB : public DeriveA |
22 |
{ |
23 |
public: |
24 |
DeriveB( ) |
25 |
{ |
26 |
} |
27 |
void vfunc( ) |
28 |
{ |
29 |
cout<<"Derive:vfunc( ) called\n"; |
30 |
} |
31 |
}; |
32 |
|
33 |
void main( ) |
34 |
{ |
35 |
DeriveB db; |
36 |
db.vfunc( ); |
37 |
} |
[编译结果]
编译通过。运行结果如下:
Base::vfunc(
) called
Derive:vfunc(
) called
[问题分析]
在构造函数和析构函数中调用虚函数时,采用静态联编,即它们所调用的虚函数是自己的类或基类中定义的函数而不是在任何派生类中重定义的函数。同样,使用对象调用虚函数也采用静态联编。
在DeriveB的对象构造时,依次调用了Base、DeriveA、DeriveB的构造函数。在调用DeriveA的构造函数时,调用了虚函数vfunc。此时,它调用的是Base中定义的vfunc函数,因为DeriveA中没有实现虚函数vfunc。所以运行结果可能和程序员的期望不同:程序员可能期望调用的是DeriveB中定义的vfunc函数。
[正确代码]
尽量不要在构造函数和析构函数里调用虚函数。当然,如果在构造函数和析构函数里调用另外的、已经构造完毕(并且尚未开始析构)的class对象的虚函数,是无可厚非的。
[例21.13] 重载虚函数。
[错误代码]
1 |
#include <iostream.h> |
2 |
class Base |
3 |
{ |
4 |
public: |
5 |
virtual void update(int n) |
6 |
{ |
7 |
cout<<"Base:int:"<<n<<endl; |
8 |
} |
9 |
virtual void update(double d) |
10 |
{ |
11 |
cout<<"Base:double::"<<d<<endl; |
12 |
} |
13 |
}; |
14 |
|
15 |
class Derive : public Base |
16 |
{ |
17 |
public: |
18 |
void update(int n) |
19 |
{ |
20 |
cout<<"Derive::int:"<<n<<endl; |
21 |
} |
22 |
}; |
23 |
|
24 |
void main( ) |
25 |
{ |
26 |
Derive d; |
27 |
d.update(12.3); |
28 |
} |
[编译结果]
编译通过。
[问题分析]
本例中,程序员在Base中提供了update函数的两种重载形式:一种参数类型为int,另一种参数类型为double。程序员决定在Derive中只让void update(int n)表现出特有行为。程序员希望调用update函数时,如果参数是整数,就调用Derive中的update函数;如果参数是double,则调用基类中的update函数。但实际在运行中,第27行代码“d.update(12.3)”将在派生类(作用域)中找到一个名为update的函数,然后发现这是个成功的匹配——只要把参数从double转换为int就行了。所以才会有如下输出结果:
Derive::int:12
[正确代码]
可以在基类Base中重载非虚函数update,然后把任务分给名字各异的虚函数UpdateInt和UpdateDouble去做:
1 |
#include <iostream.h> |
2 |
class Base |
3 |
{ |
4 |
public: |
5 |
void update(int n) |
6 |
{ |
7 |
UpdateInt(n); |
8 |
} |
9 |
void update(double
d) |
10 |
{ |
11 |
UpdateDouble(d); |
12 |
} |
13 |
protected: |
14 |
virtual void
UpdateInt(int n) |
15 |
{ |
16 |
cout<<"Base:int:"<<n<<endl; |
17 |
} |
18 |
virtual void
UpdateDouble(double d) |
19 |
{ |
20 |
cout<<"Base:double::"<<d<<endl; |
21 |
} |
22 |
}; |
23 |
|
24 |
class Derive : public Base |
25 |
{ |
26 |
public: |
27 |
void UpdateInt(int
n) |
28 |
{ |
29 |
cout<<"Derive::int:"<<n<<endl; |
30 |
} |
31 |
}; |
32 |
|
33 |
void main( ) |
34 |
{ |
35 |
Derive d; |
36 |
d.update(12.3); |
37 |
} |
[运行结果]
Base:double::12.3
[例21.14] 在构造函数和析构函数中调用纯虚函数。
[错误代码]
1 |
#include <iostream.h> |
2 |
|
3 |
class Base |
4 |
{ |
5 |
public: |
6 |
Base( ) |
7 |
{ |
8 |
Print("constructor!"); |
9 |
} |
10 |
virtual ~Base( ) |
11 |
{ |
12 |
Print("destructor!"); |
13 |
} |
14 |
virtual void Print(char* p) = 0; |
15 |
}; |
16 |
|
17 |
class Derive: public Base |
18 |
{ |
19 |
public: |
20 |
void Print(char* p) |
21 |
{ |
22 |
cout<<p<<endl; |
23 |
} |
24 |
}; |
25 |
|
26 |
void main( ) |
27 |
{ |
28 |
Derive d; |
29 |
} |
[编译结果]
Linking...
21_14.obj
: error LNK2001: unresolved external symbol "public: virtual void
__thiscall Base::Print(char *)" (?Print@Base@@UAEXPAD@Z)
Debug/21_14.exe
: fatal error LNK1120: 1 unresolved externals
[问题分析]
由于纯虚函数所在的类中没有它的定义,在该类的构造函数和析构函数中不允许调用纯虚函数,否则会导致程序运行错误。但其它成员函数可以调用纯虚函数。
本例中,基类Base在构造函数和析构函数里都调用了纯虚函数Print,由于上述原因,导致链接错误。
[正确代码]
在基类Base中,可以把Print定义为虚函数,而非纯虚函数。修改如下:
13 |
virtual void
Print(char* p) |
14 |
{ |
15 |
cout<<p<<endl; |
16 |
} |
派生类Derive修改如下:
19 |
class Derive: public Base |
20 |
{ |
21 |
}; |
[运行结果]
constructor!
destructor!
[例21.15] 从“武器”类派生出“枪”类。
[错误代码]
1 |
#include "iostream.h" |
2 |
class Weapon //“武器”类 |
3 |
{ |
4 |
public: |
5 |
virtual void Fire( ) = 0; //开火 |
6 |
}; |
7 |
|
8 |
class Gun : public Weapon //“枪”类 |
9 |
{ |
10 |
public: |
11 |
void Fire( ) //开火 |
12 |
{ |
13 |
cout<<"开火!"<<endl; |
14 |
} |
15 |
}; |
16 |
|
17 |
void main( ) |
18 |
{ |
19 |
Weapon weapon; |
20 |
weapon.Fire( ); |
21 |
} |
[编译结果]
E:\Code\21_15.cpp(19)
: error C2259: 'Weapon' : cannot instantiate abstract class due to following
members:
E:\Code\21_15.cpp(3) : see declaration of 'Weapon'
E:\Code\21_15.cpp(19)
: warning C4259: 'void __thiscall Weapon::Fire(void)' : pure virtual function
was not defined
E:\Code\21_15.cpp(5) : see declaration of 'Fire'
E:\Code\21_15.cpp(19)
: error C2259: 'Weapon' : cannot instantiate abstract class due to following
members:
E:\Code\21_15.cpp(3) : see declaration of 'Weapon'
E:\Code\21_15.cpp(19)
: warning C4259: 'void __thiscall Weapon::Fire(void)' : pure virtual function
was not defined
E:\Code\21_15.cpp(5) : see declaration of 'Fire'
[问题分析]
Weapon类包括一个纯虚函数
virtual void Fire( ) = 0;
因此是抽象类,不能实例化。抽象类也不能用作参数类型、函数返回值类型、或显式转换的类型。但是可以声明指向抽象类的指针和引用,此指针可以指向它的公有派生类,进而实现多态性。
[正确代码]
只能实例化具体类Gun:
17 |
void main( ) |
18 |
{ |
19 |
Gun gun; |
20 |
gun.Fire(
); |
21 |
} |
[运行结果]
开火!
[例21.16] 定义抽象类“生物”类,并派生出“鱼”类和“鸟”类。
[错误代码]
1 |
#include "iostream.h" |
2 |
class Creature //“生物”类 |
3 |
{ |
4 |
protected: |
5 |
int life; //平均寿命 |
6 |
int age; //当前年龄 |
7 |
public: |
8 |
Creature(int l, int a) |
9 |
{ |
10 |
life = l; |
11 |
age = a; |
12 |
} |
13 |
virtual void Info( ) = 0; |
14 |
virtual void Move( ) = 0; |
15 |
}; |
16 |
|
17 |
class Fish : public Creature //“鱼”类 |
18 |
{ |
19 |
public: |
20 |
Fish(int l, int a) : Creature(l,a) { } |
21 |
void Move( ) |
22 |
{ |
23 |
Swim( ); |
24 |
} |
25 |
void Swim( ) |
26 |
{ |
27 |
cout<<"我正在游泳!"<<endl; |
28 |
} |
29 |
}; |
30 |
|
31 |
class Bird : public Creature //“鸟”类 |
32 |
{ |
33 |
public: |
34 |
Bird(int l, int a) : Creature(l,a) { } |
35 |
void Move( ) |
36 |
{ |
37 |
Fly( ); |
38 |
} |
39 |
void Fly( ) |
40 |
{ |
41 |
cout<<"我正在飞翔!"<<endl; |
42 |
} |
43 |
}; |
44 |
|
45 |
void main( ) |
46 |
{ |
47 |
Fish fish(10, 3); |
48 |
fish.Move( ); |
49 |
|
50 |
Bird bird(15,7); |
51 |
bird.Move( ); |
52 |
} |
[编译结果]
E:\Code\21_16.cpp(47)
: error C2259: 'Fish' : cannot instantiate abstract class due to following
members:
E:\Code\21_16.cpp(17) : see declaration of 'Fish'
E:\Code\21_16.cpp(47)
: warning C4259: 'void __thiscall Creature::Info(void)' : pure virtual function
was not defined
E:\Code\21_16.cpp(13) : see declaration of 'Info'
E:\Code\21_16.cpp(47)
: error C2259: 'Fish' : cannot instantiate abstract class due to following
members:
E:\Code\21_16.cpp(17)
: see declaration of 'Fish'
E:\Code\21_16.cpp(47)
: warning C4259: 'void __thiscall Creature::Info(void)' : pure virtual function
was not defined
E:\Code\21_16.cpp(13) : see declaration of 'Info'
E:\Code\21_16.cpp(50)
: error C2259: 'Bird' : cannot instantiate abstract class due to following
members:
E:\Code\21_16.cpp(31) : see declaration of 'Bird'
E:\Code\21_16.cpp(50)
: warning C4259: 'void __thiscall Creature::Info(void)' : pure virtual function
was not defined
E:\Code\21_16.cpp(13) : see declaration of 'Info'
E:\Code\21_16.cpp(50)
: error C2259: 'Bird' : cannot instantiate abstract class due to following
members:
E:\Code\21_16.cpp(31) : see declaration of 'Bird'
E:\Code\21_16.cpp(50)
: warning C4259: 'void __thiscall Creature::Info(void)' : pure virtual function
was not defined
E:\Code\21_16.cpp(13) : see declaration of 'Info'
[问题分析]
如果派生类中给出了基类所有纯虚函数的实现,则该派生类不再是抽象类。否则,仍是抽象类。本例中,Fish和Bird类没有实现纯虚函数Info,所以这两个类仍是抽象类,不是具体类,不能实例化。编译结果告诉我们,无法实例化抽象类Fish,因为没有实现纯虚函数Info。
[正确代码]
在Fish类中实现纯虚函数Info:
|
void Info( ) |
|
{ |
|
cout<<"我是一条鱼,平均寿命大约为"<<life<<"年,"; |
|
cout<<"当前年龄为"<<age<<"岁。"<<endl; |
|
} |
在Bird类中实现纯虚函数Info:
|
void Info( ) |
|
{ |
|
cout<<"我是一只小鸟,平均寿命大约为"<<life<<"年,"; |
|
cout<<"当前年龄为"<<age<<"岁。"<<endl; |
|
} |
main函数改为:
|
void main( ) |
|
{ |
|
Fish fish(10, 3); |
|
fish.Info( ); |
|
fish.Move( ); |
|
|
|
Bird bird(15,7); |
|
bird.Info( ); |
|
bird.Move( ); |
|
} |
[运行结果]
我是一条鱼,平均寿命大约为10年,当前年龄为3岁。
我正在游泳!
我是一只小鸟,平均寿命大约为15年,当前年龄为7岁。
我正在飞翔!
请改正如下程序中的错误。
1) 写一个类CSNumber,用科学记数法表示数字,含有属性值a、b,用来表示a×10b。 重载运算符“+”、“*”,实现两个用科学记数法表示的数字的加法和乘法运算。编写main( )来测试以上代码。
1 |
#include "math.h" |
2 |
#include "iostream.h" |
3 |
|
4 |
class CSNumber |
5 |
{ |
6 |
public: |
7 |
double a; |
8 |
int b; |
9 |
CSNumber(double a=0, int b=0); |
10 |
CSNumber operator+(CSNumber n); |
11 |
friend CSNumber operator*(CSNumber n); |
12 |
}; |
13 |
CSNumber::CSNumber(double A, int B) |
14 |
{ |
15 |
a = A; |
16 |
b = B; |
17 |
} |
18 |
CSNumber CSNumber::operator+(CSNumber n) |
19 |
{ |
20 |
CSNumber temp; |
21 |
if(b>n.b) |
22 |
{ |
23 |
a = a * pow(10,b-n.b); |
24 |
b=n.b; |
25 |
} |
26 |
else if(b<n.b) |
27 |
{ |
28 |
n.a = n.a * pow(10,n.b-b); |
29 |
n.b = b; |
30 |
} |
31 |
temp.a = a + n.a; |
32 |
temp.b = b; |
33 |
|
34 |
return temp; |
35 |
} |
36 |
CSNumber operator*(CSNumber n) |
37 |
{ |
38 |
CSNumber temp; |
39 |
temp.a = a * n.a; |
40 |
temp.b = b + n.b; |
41 |
return temp; |
42 |
} |
43 |
void main(int argc, char* argv[]) |
44 |
{ |
45 |
CSNumber c1(5,10),c2(3,11),c; |
46 |
c = 5 + c2; |
47 |
cout<<"5 + ("; |
48 |
cout<<c2.a<<"*10^"<<c2.b<<") = "; |
49 |
cout<<c.a<<"X10^"<<c.b<<endl; |
50 |
|
51 |
c = c1 * c2; |
52 |
cout<<"("<<c1.a<<"*10^"<<c1.b<<") * ("; |
53 |
cout<<c2.a<<"*10^"<<c2.b<<") = "; |
54 |
cout<<c.a<<"X10^"<<c.b<<endl; |
55 |
} |
2) 定义“打印机”类,公有继承“打印机” 类,定义“针式打印机”类和“喷墨打印机”类。
1 |
#include<iostream> |
2 |
#include<string> |
3 |
using namespace std; |
4 |
|
5 |
class Cprinter //“打印机”类 |
6 |
{ |
7 |
public: |
8 |
string m_szBrand; |
9 |
bool m_bColor; |
10 |
int m_nPaperCount; |
11 |
CPrinter(string brand, bool color) |
12 |
{ |
13 |
m_szBrand = brand; |
14 |
m_bColor = color; |
15 |
} |
16 |
virtual static void Print( ) { }; |
17 |
virtual friend void Info( ) { }; |
18 |
~CPrinter( ) { } |
19 |
}; |
20 |
class CPinPrinter:public Cprinter //“针式打印机”类 |
21 |
{ |
22 |
public: |
23 |
int m_nPinCount; |
24 |
CPinPrinter(string brand, bool color, int pins) |
25 |
:CPrinter(brand, color),m_nPinCount(pins) |
26 |
{ } |
27 |
void Print( ) |
28 |
{ |
29 |
cout<<"使用针式打印机打印......打印完毕"<<endl; |
30 |
} |
31 |
void Info( ) |
32 |
{ |
33 |
if(m_bColor) |
34 |
cout<<"这是"<<m_szBrand<<"彩色针式打印机,针数"<<m_nPinCount<<endl; |
35 |
else |
36 |
cout<<"这是"<<m_szBrand<<"黑白针式打印机,针数"<<m_nPinCount<<endl; |
37 |
} |
38 |
~CPinPrinter( ) { } |
39 |
}; |
40 |
class CInkPrinter:public Cprinter //“喷墨打印机”类 |
41 |
{ |
42 |
public: |
43 |
CInkPrinter(string brnad, bool color):CPrinter(brnad, color) |
44 |
{ } |
45 |
void Print( ) |
46 |
{ |
47 |
cout<<"使用喷墨打印机打印......打印完毕"<<endl; |
48 |
} |
49 |
void Info( ) |
50 |
{ |
51 |
if(m_bColor) |
52 |
cout<<"这是"<<m_szBrand<<"彩色喷墨打印机"<<endl; |
53 |
else |
54 |
cout<<"这是"<<m_szBrand<<"黑白喷墨打印机"<<endl; |
55 |
} |
56 |
~CInkPrinter( ) { } |
57 |
}; |
58 |
void main( ) |
59 |
{ |
60 |
CPinPrinter PinPrinter("SONY",false,24); |
61 |
PinPrinter.Info( ); |
62 |
PinPrinter.Print( ); |
63 |
cout<<endl; |
64 |
|
65 |
CInkPrinter InkPrinter("HP",true); |
66 |
InkPrinter.Info( ); |
67 |
InkPrinter.Print( ); |
68 |
} |
3) 定义“立体”类,其中含有纯虚函数 “求体积”。继承该类,得到“立方体”类、“球体”类、“圆柱体”类。
1 |
#include "iostream.h" |
2 |
#define PI 3.14159265 |
3 |
class Csolid //"立体"类 |
4 |
{ |
5 |
public: |
6 |
virtual double bulk( ) = 0; //求体积 |
7 |
}; |
8 |
class CCube : public CSolid //"立方体"类 |
9 |
{ |
10 |
protected: |
11 |
float l; //边长 |
12 |
public: |
13 |
CCube(float L) |
14 |
{ |
15 |
l = L; |
16 |
} |
17 |
double bulk( ) //求体积 |
18 |
{ |
19 |
return l*l*l; |
20 |
} |
21 |
}; |
22 |
class CSphere : public CSolid //"球体"类 |
23 |
{ |
24 |
protected: |
25 |
float r;//半径 |
26 |
public: |
27 |
CSphere(float R) |
28 |
{ |
29 |
r = R; |
30 |
} |
31 |
double bulk( ) //求体积 |
32 |
{ |
33 |
return (4*PI*r*r*r)/3; |
34 |
} |
35 |
}; |
36 |
class CSylinder : public CSolid //"圆柱体"类 |
37 |
{ |
38 |
protected: |
39 |
float r; //底面半径 |
40 |
float h; //高 |
41 |
public: |
42 |
CSylinder(float R,float H) |
43 |
{ |
44 |
r = R; |
45 |
h = H; |
46 |
} |
47 |
}; |
48 |
void main(int argc, char* argv[]) |
49 |
{ |
50 |
CSolid solid; |
51 |
solid.bulk( ); |
52 |
|
53 |
CCube cube(5); |
54 |
cout<<"立方体的体积是"<<cube.bulk( )<<endl; |
55 |
|
56 |
CSphere sphere(5); |
57 |
cout<<"球体的体积是"<<sphere.bulk( )<<endl; |
58 |
|
59 |
CSylinder sylinder(5,10); |
60 |
cout<<"圆柱体的体积是"<<sylinder.bulk( )<<endl; |
61 |
} |
练习1
1) 第4行代码中把‘\n’错写成了‘/n’。
2) 去掉第6、10行代码最后多余的“;”。
3) 大括号不配对(多了两个“}”)。
练习2
1) 把第5行代码中的双引号改为单引号,把第5行代码中的‘/n’改为‘\n’。
2)
把第9行代码改为:
s=s+(float)a/(float)b;
练习3
1) 在第11、14、17行代码中,无法访问局部静态变量NO,应删除这几句代码。
2)
在func.h中,把第2行代码改为:
extern int
TotalStudents;
练习4
1) 把第14行代码中的‘=’改为‘==’。
2) 把第10行代码中的‘&’改为‘&&’。
练习5
1) 去掉第4行代码最后多余的“;”。
2) 在每个分支内部代码最后添加“break;”。
练习6
1)
在最前面增加代码
#include <stdio.h>
bool IsLeapYear(int year);
2)
在第10行代码前增加代码:
return true;
把第17行代码改为:
if(IsPrimeNumber(n)) printf("YES\n");
3) 把第5行代码中的 '\n ' 改为 '\n' 。
练习7
1) 把第2行代码改为:
#define DOUBLE(a)
2*(a)
2) 把第9行代码改为:
sum += M(i);
i++;
练习8
1) 把代码改为:
#include <stdio.h>
void main()
{
float
a[3][3],sum=0;
int i,j;
printf("请输入矩阵元素:\n");
for(i=0;i<3;i++)
for(j=0;j<3;j++)
scanf("%f",&a[i][j]);
for(i=0;i<3;i++)
sum=sum+a[i][i];
printf("矩阵对角线元素之和是:%6.2f\n",sum);
}
2) 在第12行代码前增加代码:
free(pNewArray);
把第15行代码改为:
int* p =
(int*)malloc(len*sizeof(int));
练习9
1) 第8、9行代码最后缺少“;”。
把第13、14行代码改为:
c.center.x = 5;
c.center.y = 4;
把第17行代码改为:
printf("圆心坐标:(%d,%d)\n", c.center.x, c.center.y);
2) 删除第12、13、14、16、17行代码。
练习10
1) 把第10行代码改为:
scanf("%s",
notebook.brand);
把第12行代码改为:
printf("品牌:%s\n" ,notebook.brand);
2) 把第10行代码改为:
FILE* fp1 =
fopen("pass.txt","wt");
把第11行代码改为:
FILE* fp2 =
fopen("nopass.txt","wt");
在第19行代码之前增加:
fclose(fp1);
练习11
1) 把第1行代码改为:
#include
<iostream>
2) 把第15行代码改为:
cout<<inch<<"英寸等于"<<n1::n2::InchToCM(inch)<<"厘米。"<<endl;
练习12
1) 把代码改为:
#include
<iostream.h>
void main()
{
int m,n;
cout<<"请输入两个整数:";
cin>>m;
cin>>n;
int result =
m*m+n*n;
cout<<"它们的平方和是:"<<result<<endl;
}
2) 把第7行代码改为:
cout<<"需采购"<<(count<50 ?
150-count : 0)<<"件。"<<endl;
练习13
1)
把代码改为:
#include
<iostream.h>
void main()
{
int* p = new int[5];
int * q = p;
for(int
i=0;i<5;i++)
{
cout<<q<<endl;
q++;
}
delete
p;
}
2)
在第17行代码之前增加:
delete a;
在第23行代码之前增加:
delete a;
delete b;
在第37行代码之前增加:
delete a;
delete b;
delete c;
3)
把第33行代码改为:
delete buf;
练习14
1) 把第13行代码改为:
calc(r,s,l);
2) 把第5、7、9、11行代码中的“&”去掉。
练习15
1) 把第2行代码改为:
const double
LIGHTSPEED = 300000;
把第5、6行代码改为:
return
(*ly)*LIGHTSPEED*365*24*60*60;
2) 去掉第9行代码中的“const”。
练习17
1) 把第4行代码改为:
string
FormatTime(int hour=12,int minute=0,int second=0);
把第11行代码改为:
string
FormatTime(int hour,int minute,int second)
2) 删除第5-18行代码;
把第23行代码改为:
sprintf(Time, "%d年%d月%d日 %.2d:%.2d:%.2d", year, month,
day, hour, minute, second);
练习18
1) 把第7-11行代码改为:
public:
string brand;
int size;
int speed;
int cashsize;
在第14行代码最后添加“;”。
2) 在第4行代码之前添加:
class Factory;
把第27行代码改为:
void HardDisk::Display()
3) 在第11、13、19、28行代码中去掉“void”;
删除第12、33、34、35行代码;
把第57行代码改为:
AirConditioner
ac("格力","AX-100",2000,3400.00);
4) 把第8行代码改为:
String(String&
s);
把第14行代码改为:
String::String(String&
s)
把第17行代码改为:
strcpy(m_pString,s.m_pString);
练习19
1) 把第28行代码改为:
void
ShowInfo(Website &website,Country &cn)
2) 删除第13行代码;
在第25行代码之前添加:
friend class Website;
练习20
1) 把第4行代码改为:
public:
删除第7、25、26行代码;
在第23行代码的后面添加:
:
CDigitalDevice(nElectronic, nElectricMax)
在第58行代码的后面添加:
:CDigitalDevice(nElectronic,nElectronicCapacity)
删除第60、61行代码;
2) 删除第21行代码;
把第25行代码改为:
Truck(float s,
int sc)
把第26行代码改为:
: speed(s),seatCount(sc)
把第43行代码改为:
: House(a,bc),Truck(s,sc)
练习21
1) 把代码改为:
1 |
#include "math.h" |
2 |
#include "iostream.h" |
3 |
|
4 |
class CSNumber |
5 |
{ |
6 |
public: |
7 |
double a; |
8 |
int b; |
9 |
CSNumber(double a=0, int b=0); |
10 |
friend CSNumber operator+(CSNumber n1, CSNumber n2); |
11 |
friend CSNumber operator*(CSNumber n1, CSNumber n2); |
12 |
}; |
13 |
CSNumber::CSNumber(double A, int B) |
14 |
{ |
15 |
a = A; |
16 |
b = B; |
17 |
} |
18 |
CSNumber operator+(CSNumber n1, CSNumber n2) |
19 |
{ |
20 |
CSNumber temp; |
21 |
if(n1.b>n2.b) |
22 |
{ |
23 |
n1.a = n1.a * pow(10, n1.b-n2.b); |
24 |
n1.b=n2.b; |
25 |
} |
26 |
else if(n1.b<n2.b) |
27 |
{ |
28 |
n2.a = n2.a * pow(10, n2.b-n1.b); |
29 |
n2.b = n1.b; |
30 |
} |
31 |
temp.a = n1.a + n2.a; |
32 |
temp.b = n1.b; |
33 |
|
34 |
return temp; |
35 |
} |
36 |
CSNumber operator*(CSNumber n1, CSNumber n2) |
37 |
{ |
38 |
CSNumber temp; |
39 |
temp.a = n1.a * n2.a; |
40 |
temp.b = n1.b + n2.b; |
41 |
return temp; |
42 |
} |
43 |
void main(int argc, char* argv[]) |
44 |
{ |
45 |
CSNumber c1(5,10),c2(3,1),c; |
46 |
c = 5 + c2; |
47 |
cout<<"5 + ("; |
48 |
cout<<c2.a<<"*10^"<<c2.b<<") = "; |
49 |
cout<<c.a<<"X10^"<<c.b<<endl; |
50 |
|
51 |
c = c1 * c2; |
52 |
cout<<"("<<c1.a<<"*10^"<<c1.b<<") * ("; |
53 |
cout<<c2.a<<"*10^"<<c2.b<<") = "; |
54 |
cout<<c.a<<"X10^"<<c.b<<endl; |
55 |
} |
2) 在第16行代码中去掉“static”;
在第17行代码中去掉“friend”;
把第18行代码改为:
virtual
~CPrinter() { }
把第38行代码改为:
virtual
~CPinPrinter() { }
把第56行代码改为:
virtual
~CInkPrinter() { }
3) 在第46、47行代码之间增加:
double
bulk() //求体积
{
return
PI*r*r*h;
}
删除第51、52行代码。