常宝宝 北京大学计算机科学与技术系 chbb@pku.edu.cn
参考书 《C++程序设计教程》 钱能 主编 清华大学出版社 Thinking in C++, Volume 1 Bruce Eckel 机械工业出版社 Data Structures and Program Design Robert L. Kruse & Alexander J. Ryba 高等教育出版社
关于C++ C++是一种程序设计语言 C++和C的关系: C是C++的子集 C++非常流行 C++是面向对象的程序设计语言
内容提要 C++中新增的注释方法 标准输入流和标准输出流 关于引用 引用和函数 函数重载 函数参数的默认值 内联函数 动态内存分配和回收
C++中新增的注释方法 C语言中,注释内容始于/*,止于*/,注释可以跨行,在C++中仍可使用。 在C++中,也可以用//来表示注释。 //this is the first C++ program #include <iostream.h> int main() { cout<<"Hello, World!"<<endl; }
标准输入流和标准输出流 C语言中,常使用函数printf(...) 和函数scanf(...)进行数据的输入和输出。 C++中常使用流对象进行输入和输出: (1)标准输入流对象cin (2)标准输出流对象cout cin和cout分别相当于C语言中的stdin、stdout,对应于键盘、控制台窗口,均可以被重定向。 要在程序中使用cin和cout,程序头部要包含头文件iostream.h 在控制台窗口显示信息,使用标准输出流cout的插入运算符<<。
标准输入流和标准输出流 //standard output stream #include <iostream.h> void main() { int a; …… cout << "This is the first C++ Program\n" ; cout << a << "\n"; //显示变量a的内容 }
标准输入流和标准输出流 从键盘读入数据,使用标准输入流的提取运算符>>。 输入数据的类型和提取运算符>>右边变量的类型一致。 cin和cout可以正确处理所有的基本数据类型(整型、实型、字符型、字符串),对于自定义的复杂类型,需要对运算符进行重载 …… int age; …… cout << "Please input your age: "; cin >> age; ……
标准输入流和标准输出流 显示多个数据以及输入多个数据,可以写在同一行中。 …… int a; float b; char c; cin >> a >> b >> c; cout << "a=" << a << "\n"<< "b=" << b << "\n" << "c=" << c << "\n";
标准输入流和标准输出流 在使用标准输出流时,可以使用算子改变输出格式和以及进行输出控制。 表示换行的算子为endl,如: cout << endl; //显示换行。 改变显示数制的算子: dec 十进制显示 hex 十六进制显示 oct 八进制显示 设置显示宽度的算子是setw(n),如: cout << setw(8) <<a << endl; 关于其它更多的算子,参见《C++程序设计教程》p22-27 使用算子,要包含头文件iomanip.h …… int a = 10; cout << hex << a << endl;
引用 在C++中,可以为一个变量建立引用,这相当于为该变量建立了一个别名。 引用本质是常量指针。 声明引用必须初始化,引用一旦初始化,就和一个目标(变量和对象)联系起来了。声明引用的格式如下: 类型 &引用名=变量名; …… int a; int&b = a;//为变量a建立了一个引用b
引用 引用和引用目标之间的联系一旦建立,就再也不分开: 对该引用的任何赋值,都是对引用联系的目标赋值。 对引用做求地址运算,都是对引用联系的目标求地址。 引用不是变量,通常编译器不会为引用分配存储空间。 …… int a, *pb, *pa; int&b = a;//为变量a建立了一个引用b a = 1; cout << b << endl; // 1 b = 10; cout << a << endl; // 10 pb = &b; pa = &a; // pa == pb
引用 引用的类型和其联系的目标的类型应该相同。 struct _student { char id[20]; char name[20]; int score; }; typedef struct _student STUDENT; …… STUDENT s; int& a = s; //错误
引用 引用的初始值可以不是左值(l-value),这时实际上相当于建立一个临时目标并用该初值初始化该目标,然后建立该临时目标的引用,例如: const int& i = 1; 相当于 const int temp = 1; const int& i = temp; 不能为数组建立引用,数组名本身不是一个数据类型,例如: int a[10]; int& ra[10] = a; //错误 可以为指针建立引用,例如: int *a; int b; int* & p = a; p = &b; //现在a的内容是什么?
函数参数传递 c语言中,所有函数调用均为传值调用(call by value),即在为函数传递参数时,要为函数建立参数的副本。 在传递较大的对象时,浪费时间空间;函数不能改变实参的值。 void swap( int a, int b ) { int temp; temp = a; a = b; b = temp; } int main() { int x=5, y=6; swap(x,y);// x=? Y=? ……
函数参数传递 在C语言中,可以采用指针来弥补传值调用的不足。 但采用指针的缺点是书写麻烦,可读性差。 void swap( int* a, int* b ) { int temp; temp = *a; *a = *b; *b = temp; } int main() { int x=5, y=6; swap(&x,&y);// x=? y=? ……
函数参数传递 C++增加了引用调用(call by reference)。即在定义参数时,把参数定义为引用。例如: void swap(int& a, int& b ) { … } 按照引用调用,函数不会为实参建立副本,而是把实参的地址传递给函数。(和指针传递道理相同,但写法不同) 由于引用调用不为实参建立副本,在传递较大的对象时,有效地节省了时间空间开销。由于函数得到了实参的地址,函数体对形参内容的改变会影响到实参。 在采用引用调用的函数中,对形参的无需用间接引用运算符*。,
函数参数传递 int temp;//a 和 b 相当于实参的别名 temp = a; a = b; b = temp; } void swap( int& a, int& b ) { int temp;//a 和 b 相当于实参的别名 temp = a; a = b; b = temp; } int main() { int x=5, y=6; swap(x,y);// x=? y=? ……
函数参数传递 由于引用调用会影响实参的内容,如果不希望函数体改变实参的值,可以采用const修饰符限制函数体改变实参的值。 void swap( const int& a, const int& b ) { int temp; //这个函数编译时会报错 temp = a; //因为函数体要修改实参的值 a = b; b = temp; } int main() { int x=5, y=6; swap(x,y);// x=? y=? ……
函数参数传递 在C++中,任何函数都只能有一个返回值,如果希望返回多个结果,可以采用引用调用的办法解决。(当然也可以用指针的办法) int divide(int dividend, int divisor,int& quotient,int& remainder) {//求整数相除所得的商和余数 if ( divisor==0 ) return 0; quotient = dividend / divisor; remainder = dividend % divisor; return 1; } int main() { int x = 10, y = 3, q, r, s; s = diviede( x, y, q, r );// x、y、q、r、s的值是什么?
返回引用的函数 通常函数在返回值时,实际上为返回值建立了一个副本。调用者得到的是返回值的副本,这引起时空消耗。例如: return x;//函数在返回时,建立了一个x的副本 在C++中,可以规定函数返回引用,从而不为返回值建立副本。若希望函数返回引用,可将函数的返回类型声明为引用。 int& g(int& x) { x++; return x; } int main() { int z = 6, y; y = g(z); //y和z的值是什么?
返回引用的函数 返回引用不为返回值建立副本,所以不能返回不在作用域范围内的变量或对象的引用。 int& g(int& x) { //这个函数是错误的 int a; //a是一个局部变量,作用域范围仅限于本函数 a = x; //函数执行结束后,a所占据的空间被回收,所以 a++; //变量a不再存在,故对它的引用也没有意义 return a; } int main() { int z = 6, y; y = g(z);
返回引用的函数 在函数返回引用时,函数调用可以作为左值(l-value),左值是可以出现在赋值号(=)左边的表达式。 int& g(int& x) { x++; return x; } int main() {//这个程序在C++中可以正确编译 int z = 6; g(z) = 5; //z的值是什么?
函数名重载 在C语言中,每个函数都必须有惟一的名字。如果两个函数功能相同,仅仅参数类型不同,也不能例外。 int abs( int a) {//计算整数的绝对值 if ( a >= 0 ) return a; return -a; } float fabs( float a) {//计算实数的绝对值,要采用不同的函数名 int main() { int b = -5; float c = -5.3; b = abs(b);//区别对待 c = fabs(c);//麻烦 c = abs(c); //c=? }
函数名重载 C++中允许两个函数使用相同的名字。一个名字,有了多重意义,是为重载。 同一个函数名对应多个版本,C++ 编译器自动选择正确的版本,根据 实参的类型和个数判别 int abs( int a) {//计算整数的绝对值 if ( a >= 0 ) return a; return -a; } float abs( float a) {//计算实数的绝对值,函数名相同,C++允许 int main() { int b = -5; float c = -5.3; b = abs(b); c = abs(c);// c=? }
函数名重载 int f( int );//这样的函数重载不允许 函数返回类型不能作为重载的判别依据,因为函数调用可以忽略返回值,例如: float f( int ); …… int a ; f(a); //错误, C++不能判别哪个函数将被调用
函数参数的默认值 在C++中,如果需要多次用同样的参数值去调用函数,可将此参数值指定为函数参数的默认值,从而不必每次调用时都给出这个参数值。调用时如果没有给定参数值,则使用默认值。 void delay(int loops) { //通过循环而达到等待一段时间 for ( int i = 0; i < loops; i++ ); //的目的,等待时间的长短通过 } //设定循环次数确定 int main() { …… delay(1000);…… }
函数参数的默认值 在对函数进行声明时,C++允许给函数参数设定默认值,如: 在调用时,如果没有提供参数,则使用默认值,如: void delay( int loops = 1000 ); 在调用时,如果没有提供参数,则使用默认值,如: delay();// 即delay(1000 ); 默认参数一般应在函数声明中给出,若没有函数声明,也可以在函数定义中给出。如: void delay(int loops) { //此时,函数定义中不能再有默认值 for ( int i = 0; i < loops; i++ ); } 如果函数中有多个默认参数,必须按照从右向左的顺序定义,如: void func(int a, int b=2, int c=3, int d=4 ); void func(int a=1, int b, int c=3, int d=4 ); //错误 func(10 );// func(10, 2, 3, 4 ) func(10,20 ); // func(10, 20, 3, 4 ) func(10,20,30 ); // func(10, 20, 30, 4 )
函数参数的默认值 函数默认值有时会和函数重载发生冲突,此时编译会出错,应该注意避免。 void func(int, int = 10 ); void func(int a) { ... } void func(int a, int b) { int main() { func( 5 ); //有歧义,可以解释为func(5),也可以解释 } //为func(5,10)
内联函数 函数调用是有代价的,对于程序中的任何一个函数调用,编译程序都要为其生成调用代码和进行参数传递。 有时候一个函数功能非常简单,代码短小,函数体本身的执行代价(时间和空间)还不及执行调用代码和进行参数传递的代价。此时,写成一个函数反而效率不高,不如直接把函数体写在需要调用函数的地方。 使用函数的优点是代码结构清晰。
内联函数 C语言的办法是使用宏定义。例如: 一个程序要反复判断一个字符是否是数字(‘0’-‘9’)。 #include <iostream.h> #define ISNUMBER(x) ( (x >='0' && x <='9') ?1:0 ) void main() { char c; while( ( c=cin.get() )!='\n' ) { if ( ISNUMBER( c ) ) cout << "Number" << endl; else cout << "Other" << endl; }
内联函数 但宏定义不进行类型检查。 float f; ISNUMBER(f); //编译器不会报错 宏也会产生令人厌恶的副作用,而程序员很容易忽视这一点。 #define MAX(a,b) (((a)>=(b)) ? (a):(b)) int x = 1, y = 0, r; r = MAX(x,y); //返回1 r = MAX(++x,y);// 返回3,而不是2 // r = (((++x)>=(y)) ? (++x):(y)) //宏替换是机械替换,不是首先计算参数的值,再进行替换
内联函数 C++使用内联函数,而不使用宏,但同宏一样,对于内联函数,编译器不产生函数调用代码,而是用函数体替代函数调用。 把一个函数指定为内联函数使用保留字inline。例如: inline int isnumber(char c) {//该函数是内联函数 return (c >='0' && c <='9') ?1:0; } 内联函数在替代时和宏的处理不同,要进行类型检查,对于参数要首先进行计算才进行替换。 inline int max(a,b){ return (a>=b) ? a:b; } …… int x = 1, y = 0,r; r = max(x,y); //返回1 r = max(++x,y);// 返回2 // temp = ++x; r = (temp>=y) ? temp:y;
内联函数 可以在函数声明中把函数指定为内联函数,也可以在函数定义时,把函数指定成内联函数。 内联函数应在函数调用以前进行声 明和定义,若在调用后进行定义, 将不会作为内联函数处理。 inline int isnumber(char); void main() { ... if ( isnumber(a)) { } int isnumber(char c) { void main() { //不作为内联函数 ... //对待 if ( isnumber(a)) { ... } inline int isnumber(char c) {
内联函数 并非所有函数都可以指定为内联函数,只有简单的函数才可以指定为内联函数,如果一个函数中含有循环语句,即使指定为内联函数,编译程序也不会作为内联函数对待。 不能把一个递归函数指定成内联函数。 函数实际上是一个地址,因而可以把函数赋给一个函数指针,若某函数被赋给一个函数指针,则该函数也不能指定成内联函数。即使指定,也无效。例如: inline int isnumber(char c); … int (*f)(char c); f = isnumber;
堆区内存的动态分配和回收 在C语言中要动态分配堆区(heap)内存使用malloc(...)函数,回收已经分配的堆区内存使用free(...)函数。(使用包含文件alloc.h) #include <alloc.h> …… int* p = (int*)malloc(sizeof(int)); … free(p); 这两个函数不能用于对象的内存分配,因为为对象分配内存,要调用构造函数,回收内存要调用析构函数。 C++中使用new 和delete,使用new表达式动态分配内存,使用delete表达式回收已经分配的内存。 int* p = new int; … delete p;
堆区内存的动态分配和回收 new表达式自动计算要分配类型的大小,不使用sizeof运算符,省事且可以避免错误。 如果内存分配失败,和malloc(…)一样,new表达式返回 NULL,因此可以通过检查返回值的方式得知内存分配成功与否。 int* p = new int; if ( p == NULL ) { cout << “内存分配失败! “ << endl; exit(1); } new 和 delete 必须搭配使用。用new表达式分配的内存必须用delete表达式回收。
堆区内存的动态分配和回收 new表达式在分配内存的同时,可以对内存进行初始化。例如: int *p; p=new int(100); //动态分配一个整数并初始化 cout << *p << endl; //*p的内容是什么? new表达式也可以同时为多个同类型对象分配内存,此时不能指定初值。例如: int *p; p=new int[10]; //分配一个含有10个整数的整形数组 如果使用new表达式同时为多个对象分配了内存,回收内存时,使用delete表达式中应有[]。例如: int *p; p=new int[10]; //分配一个含有10个整数的整形数组 …… delete[] p;
堆区内存的动态分配和回收 什么是内存泄漏? 程序中动态分配了内存,却没有回收,造成这部分存储空间不能再被利用,称为内存泄漏(memory leak)。 如果程序需要长期运行,内存泄漏问题会耗尽所有内存,从而使程序崩溃。 好的习惯:在所分配的内存不再使用后,及时回收内存。
上机练习内容 进一步熟悉Visual C++ 6.0的使用,学习调试功能。(单步运行程序,设置断点,监视变量内容)。 在机器上练习本节介绍的内容。