C++快速入门(一)
C++ 是一门面向对象的语言,即 OO 思想。
1. 程序示例
首先,可以在之后的代码中经常看到 using namespace std;
,这是为了调用一个叫做命名空间的东西。
C++标准库使用的所有标识符(即类、函数、对象等的名称)都是在同一个特殊的名字空间(std)中来定义的。
如果不使用上述声明,则需要在用到每个名字空间中的标识符时,在标识符名称之前加上 std::
。
1.1 示例一
题目:任意输入数字和空格,返回整数之和。
C 语言实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <stdio.h> int main (void ) { int i; int sum = 0 ; char ch; printf ("请输入一串整数和任意数目的空格:" ); while ( scanf ("%d" , &i) == 1 ) { sum += i; while ( (ch = getchar()) == ' ' ) ; if ( ch == '\n' ) { break ; } ungetc( ch, stdin ); } printf ("结果是:%d\n" , sum); return 0 ; }
C++实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <iostream> using namespace std; int main () { int sum = 0 ; cout << "请输入一串整数和任意数目的空格:" ; int i; while ( cin >> i ) { sum += i; while ( cin.peek () == ' ' ) cin.get (); if ( cin.peek () == '\n' ) break ; } cout << "结果是:" << sum << endl; return 0 ; }
“>>” 在 C 中定义为右移操作符,它在 C++ 中进行了重载,当它按照这里所示的方法使用时,它就用于从输入流对象提取信息。
该操作符对所有内键的数据类型都进行了重载,所以它可以从输入流对象提取出 int,float,double 型数据,也可以提取字符串等数据。
1.2 示例二
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <iostream> using namespace std;int main () { char buf[20 ]; cin.ignore (7 ); cin.getline (buf, 10 ); cout << buf << endl; return 0 ; }
1.3 示例三
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <iostream> using namespace std;int main () { const int SIZE = 50 ; char buf[SIZE]; cout << "请输入一段文本:" ; cin.read (buf, 20 ); cout << "字符串收集到的字符数为:" << cin.gcount () << endl; cout << "输入的文本信息是:" ; cout.write (buf, 20 ); cout << endl; return 0 ; }
1.4 示例四
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <iostream> #include <math.h> using namespace std;int main () { double result = sqrt (3.0 ); cout << "对 3 开方保留小数点后 0~9 位,结果如下:\n" << endl; for (int i = 0 ; i <= 9 ; i++) { cout.precision (i); cout << result << endl; cout << "当前的输出精度为:" << cout.precision () << endl; } return 0 ; }
1.5 示例五
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <iostream> using namespace std;int main () { int width = 4 ; char str[20 ]; cout << "请输入一段文本:\n" ; cin.width (5 ); while (cin >> str) { cout.width (width++); cout << str << endl; cin.width (5 ); } return 0 ; }
2. 文件操作
2.1 拷贝文件
copy 源文件 目的文件名
2.2 C 语言形式,检查文件 copy 输入是否正确
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include <stdio.h> #include <stdlib.h> int main ( int argc, char *argv[] ) { FILE *in, *out; int ch; if ( argc != 3 ) { fprintf (stderr , "输入形式:copyFile 源文件名 目标文件名\n" ); exit ( EXIT_FAILURE ); } if ( (in = fopen(argv[1 ], "rb" ) ) == NULL ) { fprintf (stderr , "打不开文件:%s \n" , argv[1 ]); exit ( EXIT_FAILURE ); } if ( ( out = fopen( argv[2 ], "wb" ) ) == NULL ) { fprintf ( stderr , "打不开文件:%s \n" , argv[2 ] ); fclose( in ); exit ( EXIT_FAILURE ); } while ( ( ch = getc( in ) ) != EOF ) { if ( putc( ch, out ) == EOF ) { break ; } } if ( ferror( in ) ) { printf ("读取文件 %s 失败!\n" , argv[2 ]); } printf ("成功复制 1 个文件!\n" ); fclose( in ); fclose( out ); return 0 ; }
stderr
跟 stdin
类似,不过它指向的是屏幕这个“文件”,专门用来报错。
fprintf(stderr, "输入形式: copyfile 源文件名 目标文件名")
这行代码相当于 printf("输入形式: copyfile 源文件名 目标文件名")
。
argc
与 argv[]
在程序中,main 函数有两个参数,整数变量 argc
和字符指针数组 argv[]
。
argc
的含义是程序的参数数量,包含本身。
argv[]
的每个指针指向命令行的一个字符串,所以 argv[0]
指向字符串 "copyFile.exe"
,argv[1]
指向字符串 sourceFile,argv[2]
指向字符串 destFile 。
ps: 感觉上面这种码风好好看,代码看起来胖胖的,条件语句的括号里前后加空格结构也变清晰了,向这个方向改进。
2.3 fstream
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <fstream> #include <iostream> using namespace std;int main () { ifstream in; in.open ( "test.txt" ); if ( !in ) { cerr << "打开文件失败" << endl; return 0 ; } char x; while ( in >> x ) { cout << x; } cout << endl; in.close (); return 0 ; }
一开始没有创建 test.txt
显示打开失败。创建后,将文本内容读取出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <fstream> #include <iostream> using namespace std;int main () { ofstream out; out.open ( "test.txt" ); if ( !out ) { cerr << "打开文件失败!" << endl; return 0 ; } for ( int i = 0 ; i < 10 ; i++ ) { out << i; } out << endl; out.close (); return 0 ; }
可以看到文件正常写入了,并且之前的内容被覆盖了。
2.4 fstream 续
1 2 ifstream in ("test.txt" ) ;ofstream out ("test.txt" ) ;
如上,可以在创建 ifstream
和 ofstream
类的对象时,将文件的名字传递给它们的构造函数(对象默认使用的函数方法)。
此用法和前面的用法没什么区别,事实上,它还可以接收不知一个参数!
1 ifstream in (char * filename, int open_mode) ;
其中 filename
表示文件名称,open_mode
表示打开模式。
下面给出几种常见的打开模式:
1 2 3 4 5 6 7 ios::in ios::out ios::binary ios::app ios::trunk ios::nocreate ios::noreplace
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <fstream> #include <iostream> using namespace std;int main () { ofstream out; out.open ( "test.txt" , ios::app ); if ( !out ) { cerr << "打开文件失败!" << endl; return 0 ; } for ( int i = 10 ; i > 0 ; i-- ) { out << i; } out << endl; out.close (); return 0 ; }
可以看到文件中追加了一行。
如何同时满足两种方式?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <fstream> #include <iostream> using namespace std;int main () { fstream fp ( "test.txt" , ios::in | ios:: out ) ; if (!fp) { cerr << "打开文件失败!" << endl; return 0 ; } fp << "Iloveawellfrog.cc!" ; static char str[50 ]; fp.seekg (ios::beg); fp >> str; cout << str << endl; fp.close (); return 0 ; }
为什么 |
连接的两种打开模式可以同时执行呢?
这是由于底层定义方式导致的:
可以看到底层定义是个枚举类型,每种类型只检查对应位是否为 1,如果同时多个位为 1 ,则同时执行多种方式。
光看不练没有长进滴!
2.4.1 练习1
题目:程序向用户提出一个“Y/N”问题,然后把用户输入的值赋给 answer 变量。
要求用户输入大小写均可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <iostream> int main () { char answer; std::cout << "请问我可以格式化你的硬盘吗?!\n" ; std::cin >> answer; switch (answer) { case 'Y' : case 'y' : std::cout << "随便格式化硬盘你是会后悔滴~\n" ; break ; case 'N' : case 'n' : std::cout << "你的选择是明智的!\n" ; break ; default : std::cout << "你输入的啥玩意,我根本不认识?!\n" ; break ; } std::cin.ignore (100 , '\n' ); std::cout << "输入任意操作符结束" << std::endl; std::cin.get (); return 0 ; }
3. 函数的重载(overloading)
函数重载: 实质上就是同样名字再定义一个有着不同参数同样用途的函数。
注意:可以是参数 个数 的不同,也可以是参数 类型 的不同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <iostream> using namespace std;void myPrint (int num) ;void myPrint (float num) ; int main () { int numInt = 3 ; float numFloat = 5.1 ; myPrint (numInt); myPrint (numFloat); return 0 ; } void myPrint (int num) { cout << num << endl; } void myPrint (float num) { cout << num << endl; }
对函数的重载,有时可以像上面的例子一样,简化变成工作、提高代码可读性。
有些点需要注意:
程序中不能随意对函数(方法)进行重载。
重载函数越多,该程序就越不容易看懂。
注意区分重载和覆盖。(覆盖就是函数名相同,传入参数也相同。)
对函数进行重载的目的是为了方便对不同数据类型进行同样的处理。
思考:为什么不能通过返回值不同来重载函数(方法)呢?
答:虽然返回值不同可以区分函数,但是在 main()
函数之前的声明是相同的,这会让编译器懵逼。。。
4. 复杂数据类型
下面这个例子主要学习一下输入保护,防止用户违法输入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <iostream> int main () { const unsigned short ITEM = 10 ; int num[ITEM]; std::cout << "请输入" << ITEM << "个整数数据!\n\n" ; for (int i = 0 ; i < ITEM; i++) { std::cout << "请输入第" << i + 1 << "个数据:" ; while ( !(std::cin >> num[i]) ) { std::cin.clear (); std::cin.ignore (100 , '\n' ); std::cout << "请输入一个合法的值" << std::endl; } } return 0 ; }
C++ 中有字符串数据类型 string
可以替代 C 中的字符数组,使用起来更方便。
还有一些内置的方法函数:
提取子字符串
比较字符串
添加字符串
搜索字符串
等等
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <iostream> #include <string> int main () { std::string str; std::cout << "请随意输入一个字符串:" ; std::getline (std::cin, str); std::cout << str << std::endl; return 0 ; }
指针
对齐: 在计算机底层,对齐比比皆是。内存对齐,文件对齐。(程序在编译链接之后会被分割成一个一个的区块,而区块在文件和内存中要按照一定规律来对齐)
一般 32 位系统内存对齐值是 : 1000H == 4KB(一页)
一般 64 位系统内存对齐值是 : 2000H == 8KB
文件对齐值是:200H(文件中存放着文件内容,不需要立即提交,为了节省内存,所以存放的更紧凑。)
关于指针的声明:
1 2 int *p1, p2, p3; int *p1, *p2, *p3;
建议不要怕多写几行,多出来的回车和空格会被编译器优化掉,不会影响程序内存的大小。
思考:既然指针类型的大小和 int
类型一样,为什么不直接用 int
类型存放地址,而要专门创建一个指针类型呢?
1、直接访问硬件
2、快速传递数据(指针表示地址)
3、返回一个以上的值返回一个(数组或者结构体的指针)
4、表示复杂的数据结构(结构体)
5、方便处理字符串
结构体指针示例
https://fishc.com.cn/thread-11989-1-1.html
5. 传值、传址、传引用
传值: 函数直接传递值,是形参,不会在调用的函数中修改原来的数值。
传址: 为了在调用函数中修改原值,使用指针传递地址。
传引用: 不使用指针,修改原值。(从汇编来看,还是传递地址。)
传引用实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <iostream> using namespace std;void swap (int &a, int &b) ;int main () { int a = 1 ; int b = 3 ; swap (a, b); cout << a << " " << b << endl; return 0 ; } void swap (int &a, int &b) { int temp = a; a = b; b = temp; }
栈溢出示例
1 2 3 4 5 6 7 8 9 #include <stdio.h> int main (void ) { char array [10 ]; scanf ("%s" , array ); printf ("%s\n" , array ); return 0 ; }
可以看到,程序运行出现了问题。这是因为栈的空间分配是自上而下的,输入的字符串超过了 array 的长度,就覆盖了 main
函数存储的地址,return
到了被覆盖的奇怪地址,所以程序崩溃了。
6. 对象
类由变量和函数组成,对象将使用那些变量(属性)来存储信息,调用那些函数(方法)来完成操作。
给类添加方法:
先在类的声明里创建一个方法的原型
稍后再实现这个方法
1 2 3 4 5 6 7 8 9 10 class Car {public : std::string color; std::string engine; float gas_tank; unsigned int Wheel; void fill_tank (float liter) ; };
上面的类中只声明了方法,要想使用该方法,还需要对这个函数进行正式的定义。方法的定义通常安排在类的声明后面。
1 2 3 void Car::fillTank (float liter) { gas_tank += liter; }
作用域解析符 ::
,作用是告诉编译器这个方法属于哪一个类。
std::cout
其实就是在使用对象。
举个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 #include <iostream> #include <windows.h> #define FULL_GAS 85 class Car {public : std::string color; std::string engine; unsigned int gas_tank; unsigned int wheel; void setColor (std::string col) ; void setEngine (std::string eng) ; void setWheel (unsigned int whe) ; void fillTank (int liter) ; int running (void ) ; void warning (void ) ; }; void Car::setColor (std::string col) { color = col; } void Car::setEngine (std::string eng) { engine = eng; } void Car::setWheel (unsigned int whe) { wheel = whe; } void Car::fillTank (int liter) { gas_tank += liter; } int Car::running (void ) { std::cout << "我正在以“全力以赴”的时速往前移动。。。跨过山和大海奔向美好的未来!\n" ; gas_tank--; std::cout << "当前还剩 " << 100 * gas_tank / FULL_GAS << "%" << "油量!\n" ; return gas_tank; } void Car::warning (void ) { std::cout << "WARNING!!" << "还剩" << 100 * gas_tank / FULL_GAS << "%" << "油量!" ; } int main () { char i; Car mycar; mycar.setColor ("WHITE" ); mycar.setEngine ("V8" ); mycar.setWheel (4 ); mycar.gas_tank = FULL_GAS; while (mycar.running ()) { if (mycar.running () < 10 ) { mycar.warning (); std::cout << "请问是否需要加满油再继续行驶?(Y/N)\n" ; std::cin >> i; if ('Y' == i || 'y' == i) { mycar.fillTank (FULL_GAS); } } } return 0 ; }
7. 构造器和析构器
构造器
构造器是一个特殊的方法,常常用来初始化一些类的属性。
构造器和通常方法的主要区别:
构造器的名字和它所在的类的名字一样
系统在创建某个类的实例时会第一时间自动调用这个类的构造器
构造器永远不会返回任何值
创建构造器,需要先把它的声明添加到类里
注意大小写与类名保持一致,在结束声明之后开始定义构造器本身
1 2 3 4 5 6 7 8 9 10 class Car { Car (void ); } Car::Car (void ) { color = "WHITE" ; engine = "V8" ; wheel = 4 ; gas_tank = FULL_GAS; }
修改上面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 #include <iostream> #include <windows.h> #define FULL_GAS 85 class Car {public : std::string color; std::string engine; unsigned int gas_tank; unsigned int wheel; Car (void ); void setColor (std::string col) ; void setEngine (std::string eng) ; void setWheel (unsigned int whe) ; void fillTank (int liter) ; int running (void ) ; void warning (void ) ; }; Car::Car (void ) { color = "While" ; engine = "V8" ; wheel = 4 ; gas_tank = FULL_GAS; } void Car::setColor (std::string col) { color = col; } void Car::setEngine (std::string eng) { engine = eng; } void Car::setWheel (unsigned int whe) { wheel = whe; } void Car::fillTank (int liter) { gas_tank += liter; } int Car::running (void ) { char ch; std::cout << "我正在以“全力以赴”的时速往前移动。。。跨过山和大海奔向美好的未来!\n" ; gas_tank--; std::cout << "当前还剩 " << 100 * gas_tank / FULL_GAS << "%" << "油量!\n" ; if (gas_tank < 10 ) { std::cout << "请问是否需要加满油再行驶?(Y/N)\n" ; std::cin >> ch; if ('Y' == ch || 'y' == ch) { fillTank (FULL_GAS); } if (0 == gas_tank) { std::cout << "抛锚中。。。。。。" ; return 1 ; } } return 0 ; } void Car::warning (void ) { std::cout << "WARNING!!" << "还剩" << 100 * gas_tank / FULL_GAS << "%" << "油量!" ; } int main () { Car mycar; while (!mycar.running ()) { ; } return 0 ; }
每个类至少有一个构造器,如果没有在类中定义构造器,编译器就会使用如下语法替你定义一个:ClassName::ClassName(){}
这是一个没有代码内容的空构造器。
除此之外,编译器还会替你创建一个副本构造器(CopyConstructor)。
析构器
构造器 用来完成事先的初始化和准备工作(申请分配内存)。
析构器 用来完成事后所必须的清理工作(清理内存)。
析构器和构造器相辅相成:
析构器有着和构造器(类)一样的名字, 只不过前面多了一个波浪符 ~
前缀。
析构器永远不返回任何值
析构器是不带参数的,所以声明格式为 ~ClassName();
刚刚的例子中析构器可有可无。但是在较复杂的类中,析构器往往至关重要,可能由于类的空间没有释放,而被占用无法访问(内存泄漏)。
例如,某个类的构造器申请了一块内存,我们就必须在析构器里释放那块内存。
1 2 3 4 class Car { Car (void ); ~Car (); }
举个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #include <iostream> #include <string> #include <fstream> class StoreQuote {public : std::string quote, speaker; std::ofstream fileOutput; StoreQuote (); ~StoreQuote (); void inputQuote () ; void inputSpeaker () ; bool write () ; }; StoreQuote::StoreQuote () { fileOutput.open ("test.txt" , std::ios::app); } StoreQuote::~StoreQuote () { fileOutput.close (); } void StoreQuote::inputQuote () { std::getline (std::cin, quote); } void StoreQuote::inputSpeaker () { std::getline (std::cin, speaker); } bool StoreQuote::write () { if (fileOutput.is_open ()) { fileOutput << quote << "|" << speaker << "\n" ; return true ; } else { return false ; } } int main () { StoreQuote quote; std::cout << "请输入一句名言:\n" ; quote.inputQuote (); std::cout << "请输入作者:\n" ; quote.inputSpeaker (); if (quote.write ()) { std::cout << "成功写入文件^_^" ; } else { std::cout << "写入文件失败T_T" ; return 1 ; } return 0 ; }
运行成功,在文件中就可以看到写入的内容了。
8. this 指针和类的继承
this 指针
对象中有一个特殊的指针叫 this
,下面通过一个例子认识它:
1 2 3 4 5 6 7 8 class Human { char awellfrog; Human (char awellfrog); }; Human::Human (char awellfrog) { awellfrog = awellfrog; }
可以看到 Human()
构造器有一个名为 awellfrog
的参数。这里虽然名字和 Human
类里的属性同名,却是不相干的两样东西,所以并没有报错。问题是构造器不知道是覆盖属性还是覆盖参数。这时就要用到 this
指针了:
1 this ->awellfrog = awellfrog;
现在编译器就明白了,赋值操作符的左边将被解释为当前对象的 awellfrog
属性,右边将被解释为构造器的传入参数 awellfrog
。
注意: 使用 this
指针的基本原则是:如果代码不存在二义性的隐患,就不必使用 this
指针!
this
指针也会在一些更加高级的方法里用到。
类的继承
通过继承机制,程序员可以对现有的代码进行进一步的扩展,并应用在新的程序中。
编写一个 Animal
类作为 Turtle
类和 Pig
类的基类。
继承在 C++ 中的写法如下:
1 2 3 class SubClass : public SuperClass {...}e.g. class Pig : public Animal {...}
举例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 #include <iostream> #include <string> class Animal {public : std::string mouth; void eat () ; void sleep () ; void drool () ; }; class Pig : public Animal {public : void climb () ; }; class Turtle : public Animal {public : void swim () ; }; void Animal::eat () { std::cout << "I'm eatting!" << std::endl; } void Animal::sleep () { std::cout << "I'm sleeping! Don't disturb me!" << std::endl; } void Animal::drool () { std::cout << "公的看到母的流口水。。。(小黄鱼这个例子真绝)" << std::endl; } void Pig::climb () { std::cout << "母猪会上树" << std::endl; } void Turtle::swim () { std::cout << "乌龟会游泳" << std::endl; } int main () { Pig pig; Turtle turtle; pig.eat (); turtle.eat (); pig.climb (); turtle.swim (); return 0 ; }
9. 继承机制中的构造器和析构器
当使用继承机制时,构造器和析构器就变得有点复杂了。
比如基类有个构造器,他将在创建子类对象时最先被调用,然后才调用子类的构造器。因为基类必须在子类之前初始化!
如果构造器带着输入参数,事情就变得稍微复杂了。
当调用 Pig()
构造器时(以 theName
作为输入参数),Animal()
构造器也将被调用(theName
输入参数将传递给它)。
当调用 Pig pig("小猪猪")
时,把字符串“小猪猪”传递给 Pig()
和 Animal()
,赋值动作将实际发生在 Animal()
方法里。
1 2 3 4 5 6 7 8 9 10 class Animal {public : Animal (std::string theName); std::string name; } class Pig : public Animal {public : Pig (std::string theName); }
具体实例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 #include <iostream> #include <string> class Animal {public : std::string mouth; std::string name; Animal (std::string theName); void eat () ; void sleep () ; void drool () ; }; class Pig : public Animal {public : void climb () ; Pig (std::string theName); }; class Turtle : public Animal {public : void swim () ; Turtle (std::string theName); }; Animal::Animal (std::string theName) { name = theName; } void Animal::eat () { std::cout << "I'm eatting!" << std::endl; } void Animal::sleep () { std::cout << "I'm sleeping! Don't disturb me!" << std::endl; } void Animal::drool () { std::cout << "公的看到母的流口水。。。(小黄鱼这个例子真绝)" << std::endl; } Pig::Pig (std::string theName) : Animal (theName) {} void Pig::climb () { std::cout << "母猪会上树" << std::endl; } Turtle::Turtle (std::string theName) : Animal (theName) {} void Turtle::swim () { std::cout << "乌龟会游泳" << std::endl; } int main () { Pig pig ("小猪猪" ) ; Turtle turtle ("甲鱼" ) ; std::cout << "这只猪的名字是:" << pig.name << std::endl; std::cout << "每只乌龟都有个伟大的名字:" << turtle.name << std::endl; pig.eat (); turtle.eat (); pig.climb (); turtle.swim (); return 0 ; }
销毁某个对象时,基类的析构器也将被自动调用,但这些事编译器会自动替你处理。
与构造器的情况相反,基类的析构器将在子类的最后一条语句执行完毕后才被调用。
构造器和析构器的调用过程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #include <iostream> #include <string> class BaseClass {public : BaseClass (); ~BaseClass (); void doSomething () ; }; class SubClass : public BaseClass {public : SubClass (); ~SubClass (); }; BaseClass::BaseClass () { std::cout << "进入基类构造器。。。\n" ; std::cout << "我在基类构造器里边做事...\n\n" ; } BaseClass::~BaseClass () { std::cout << "进入基类析构器。。。\n" ; std::cout << "我在基类析构器里做事...\n\n" ; } void BaseClass::doSomething () { std::cout << "我干了某些事。。。。。。\n\n" ; } SubClass::SubClass () { std::cout << "进入子类构造器...\n" ; std::cout << "我在子类构造器里做事。。。\n\n" ; } SubClass::~SubClass () { std::cout << "进入子类析构器...\n" ; } int main () { SubClass subclass; subclass.doSomething (); std::cout << "完工,收工!\n" ; return 0 ; }
运行程序,可以看到基类与子类中构造器与析构器的调用过程如下:
10. 访问控制
关于构造器的设计越简明越好!我们应该只用它来初始化各种有关的属性。
作为一个基本原则,在设计、定义和使用一个类的时候,应该让它的每个组成部分简单到不能再简单!
别忘了,析构器的基本用途是对前面所做的事情进行清理。尤其是在使用了动态内存的程序里,析构器将至关重要。
C++ 的访问级别:
如果一个对象试图访问它没有权限的内容,编译器会报错。
级别
允许谁来访问
public
任何代码
protected
这个类本身和它的子类
private
只有这个类本身
示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 int main () {''' Pig pig ("小猪猪" ) ; Turtle turtle ("甲鱼" ) ; pig.name = "小姑凉" ; std::cout << "这只猪的名字是:" << pig.name << std::endl; std::cout << "每只乌龟都有个伟大的名字:" << turtle.name << std::endl; ''' }
我们可以看到,漂亮的小姑凉怎么能是 pig 的名字呢,这样是不行的,于是我们对 name
属性设置为 protected
1 2 3 4 5 6 7 8 9 10 11 12 class Animal {public : std::string mouth; Animal (std::string theName); void eat () ; void sleep () ; void drool () ; protected : std::string name; };
再运行,我们就可以看到这里报错了,说 name
属性被保护了。
使用 private 的好处是,今后只修改某个类的内部实现,而不必重新修改整个程序。这时因为其他代码根本访问不到 private 保护的内容,所以不怕“牵一发而动全身”的惨剧发生!
同一个类定义里可以使用多个 public:
, private:
, protected:
语句,但最好把同类型的元素集中在一起,这样代码的可读性会好很多。
在编写类定义代码时,应该从 public:
开始写起,然后是 protected:
,最后是 private:
。
虽然编译器并不挑剔这些顺序,但这么做的好处是——好的顺序可以节省大量的时间。
11. 覆盖方法和重载方法
覆盖(基类声明,子类中重新声明并修改)
光有继承在有些场景下是不够用的。例如当我们在基类里提供了一个通用的函数,但是它的某个子类里需要修改这个方法的实现,在 C++ 里,覆盖(overriding) 就可以做到。
例如,虽然动物都会吃,但是不同动物会吃不同的东西,修改之前的代码如下:
只需要在类里重新声明这个方法,然后再改写一下它的实现代码(就像它是一个增加的方法那样)就行了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 class Animal {public : std::string mouth; Animal (std::string theName); void eat () ; void sleep () ; void drool () ; protected : std::string name; }; class Pig : public Animal {public : Pig (std::string theName); void climb () ; void eat () ; }; class Turtle : public Animal {public : Turtle (std::string theName); void swim () ; void eat () ; }; ''' void Pig::eat () { Animal::eat (); std::cout << name << "正在吃鱼!\n\n" ; } ''' void Turtle::eat () { Animal::eat (); std::cout << name << "正在吃东坡肉!\n\n" ; }
可以看到子类中的方法覆盖了基类的方法。
重载(定义位置相同,名字相同,输入参数不同,直接继承,不用再子类中再次声明)
重载机制使你可以定义多个同名的方法(函数),只是它们的输入参数必须不同。(因为编译器是依靠不同的输入参数来区分不同的方法)。
重载可以简化编程工作,提高代码可读性。
重载并不是真正的面向对象特性,它只是可以简化编程工作的捷径,而简化编程工作正是 C++ 全部追求。
看一个重载的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 ''' class Animal {public : std::string mouth; Animal (std::string theName); void eat () ; void eat (int eatCount) ; void sleep () ; void drool () ; protected : std::string name; }; class Pig : public Animal {public : Pig (std::string theName); void climb () ; }; class Turtle : public Animal {public : Turtle (std::string theName); void swim () ; }; ''' void Animal::eat () { std::cout << "I'm eatting!" << std::endl; } void Animal::eat (int eatCount) { std::cout << "我吃了" << eatCount << "混沌" << std::endl; } ''' int main () { Pig pig ("小猪猪" ) ; Turtle turtle ("甲鱼" ) ; pig.eat (15 ); pig.eat (); turtle.eat (); pig.climb (); turtle.swim (); return 0 ; }
1. 一定要合理使用重载,重载越多,程序就越不容易看懂。
2. 在对方法进行覆盖时,一定要看仔细,千万不要把输入参数打多或者打少了,因为只要声明的输入参数和返回值与原来的不一致,你编写的就是一个重载方法而不是覆盖方法。这种错误往往很难调!!!因为没有语法错误。
子类只能覆盖,不能重载基类的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 ''' class Animal {public : std::string mouth; Animal (std::string theName); void eat () ; void sleep () ; void drool () ; protected : std::string name; }; class Pig : public Animal {public : Pig (std::string theName); void climb () ; void eat (int eatCount) ; }; ''' void Animal::eat () { std::cout << "I'm eatting!" << std::endl; } ''' void Pig::eat (int eatCount) { std::cout << "我吃了" << eatCount << "混沌" << std::endl; } ''' int main () { Pig pig ("小猪猪" ) ; Turtle turtle ("甲鱼" ) ; pig.eat (15 ); pig.eat (); turtle.eat (); pig.climb (); turtle.swim (); return 0 ; }
可以看到这里报错了,提示需要一个参数,而没有匹配到 Pig::eat()
,可见基类的 eat()
方法被子类覆盖了,原方法不在了。
12. 友元关系
在某些场合一个完全无关的类由于某些特殊原因需要访问到某个 protected
成员,甚至某个 private
成员,怎么办呢?
如果全部设置成 public
那就失去了保护的作用。
于是 友元关系 应运而生。
友元关系: 类之间的一种特殊关系,这种关系不仅允许友元类访问对方的public方法和属性,还允许友元访问对方的 protected
和 private
方法和属性。(——非常亲密的关系呦)
声明一个友元关系只需要在类声明里的某个地方加上一条 friend class **
就可以。
注: 这条语句可以放在任何地方,放在 public
, protected
,private
段落里都可以。
下面举个例子练习一下:
爸爸可以教孩子,也可以让孩子去办事。
爸爸的兄弟是孩子的长辈,也可以让孩子去办些简单的事。
设置爸爸的兄弟是父子类的友元。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 #include <iostream> #include <string> class FatherAndSon {public : FatherAndSon (std::string theName); void teach (FatherAndSon *child) ; void ask (FatherAndSon *child, std::string something) ; protected : std::string name; friend class Brothers ; }; class Father : public FatherAndSon {public : Father (std::string theName); }; class Son : public FatherAndSon {public : Son (std::string theName); }; class Brothers {public : Brothers (std::string theName); void ask (FatherAndSon *child, std::string something) ; protected : std::string name; }; FatherAndSon::FatherAndSon (std::string theName) { name = theName; } void FatherAndSon::teach (FatherAndSon *child) { std::cout << child->name << ",好孩子,来。" << name << "教你点东西" << std::endl; } void FatherAndSon::ask (FatherAndSon *child, std::string something) { std::cout << name << "让" << child->name << "去" << something << std::endl; } Father::Father (std::string theName) : FatherAndSon (theName) {} Son::Son (std::string theName) : FatherAndSon (theName) {} Brothers::Brothers (std::string theName) { name = theName; } void Brothers::ask (FatherAndSon *child, std::string something) { std::cout << child->name << "帮" << name << "去" << something << std::endl; } int main () { Father father ("爸爸" ) ; Son son ("小花" ) ; Brothers fatherbrother ("大爹" ) ; father.teach (&son); father.ask (&son, "买包烟" ); std::cout << "\n爸爸的兄弟,小花的大爹来啦!\n" ; fatherbrother.ask (&son, "买包辣条" ); return 0 ; }
可以看到爸爸的兄弟可以让小花去买辣条。
但是如果把上面的友元声明 friend class Brothers;
注释掉,就会报错:大爹没法通过 child
调用 FatherAndSon
类的属性 name
。(同时注意到,继承了 FatherAndSon
的 Father
和 Son
子类可以通过其对象调用 protected
的属性 name
)