声明:该笔记基于作者对C++理解对所写,因此会存在错误与不足,望各位指出与修正。学习过程中使用的教材为《第三版C++语言程序设计教程》,在此感谢VC驿站Syc与玄马教育提供的视频的帮助。
一、面向对象程序设计和C++
标签(空格分隔):概念 特点 实例
1.1 面向对象程序设计(OOP)的基本概念
面向过程的软件开发过于强调分析问题的功能而忽略了数据和功能之间的内在联系。
面向对象的方法是对面向过程程序设计方法的继承和发展,强调直接以问题域(现实世界)中的事物为中心来思考问题、认识问题。以下是面向对象领域中几个重要的概念:
1:对象
客观世界由各种对象组成,对象是现实世界中一个实际存在的事物。对象具有静态特征和动态特征(静态特征是用数据来描述的特征;动态特征为对象所表现的行为或具有的功能)。
对象由一组属性和对这组属性进行操作的一组服务构成。(属性是用来描述对象静态特性的数据项;服务是用来描述对象动态特性的操作序列)。
对象是由生命的,即从出生(创建)-> 生长(活动) -> 灭亡(删除)。
2:类
C++也称"带类的C"。面向对象的类是具有相同属性和行为特征的一组对象的集合。它为该类的全部对象提供了属性和方法的抽象描述。(属性是类的静态特征;方法是类的某些操作行为的实现,是说明实现该行为的算法和过程)。
关系:某个类中任何一个对象都是该类的一个具体实例。
在同一个类的不同对象之间具有以下特征:
(1)相同的属性;
(2)相同的方法;
(3)不同的对象名;
(4)不同的属性值。
3:消息
对象间通过消息进行相互通信。可以一对一或一对多进行通信也可是一个对象调用另一个对象的操作。
消息是对象在一次交互中所传递的信息。在面向对象的方法中,把对象发出的服务请求就称为消息。
1.2 面向对象的基本特征
1:抽象性
面向对象的方法的基本特征是抽象性,将具有相同属性和行为的一组对象抽象为类,由类的定义和对象的使用构成面向对象程序的基本框架。
2:封装
封装具有两重含义:第一个含义是把对象的属性和服务结合成一个独立的系统单位(对象);第二个含义也称为“信息隐蔽”,即尽可能的隐藏对象的内部细节,只保留有限的对外接口使之与外部发生联系。
3:继承
指特殊类拥有其一般类的全部属性与服务。表明特殊类不必重新定义已在其他一般类中定义过的属性和行为,而它却自动地、隐含地拥有其一般类的所有属性与行为。一般类通常称为基类或父类,将特殊类称为派生类或子类
4:多态性
指在一般类中定义的属性或行为,被特殊类继承之后,可以具有不同的数据类型或表现出不同的行为。
1.3 C++程序实例
功能:显示“Hello World”
#include <iostream> //载入头文件
using namespace std; //使用命名空间
int main() //程序入口
{
cout<<"Hello World"<<endl;
return 0;
}
程序解释:
“#include <iostream>
”,此语句以#开头,是一条预处理指令。此条指令将iostream头文件包含到程序当中,该头文件包含了用于输入/输出处理的对象与函数原型,凡是程序需要进行输入/输出处理,都要包含该头文件。
main()称为主函数,是程序的入口,表示程序从此处开始执行,每个程序只有一个入口。
"using namespace std;"表示使用std名字空间。
C++提供了名字空间将相同的名字放在不同的名字空间中来防止名字冲突
例:
#include <iostream>
using namespace std;
namespace my
{
int money = 100;
};
namespace your
{
int money = 50;
};
int main()
{
cout<<"my money = "<<my::money<<endl;
cout <<"your money = "<<your::money<<endl;
return 0;
}
输出结果:
my money = 100
your moeny = 50
-
cin/cout 简介:
cin(读作see-in):为标准输入流对象,通常代表键盘输入
格式:cin>>对象1>>对象2>>…>>对象n;
作用:将键盘上提取的n个数据分别依次给对象1,对象2…,对象n.
cout(读作see-out):为标准输出流对象,通常代表显示设备。
格式:cout<<对象1<<对象2<<…<<对象n;
作用:依次将对象1,对象2,…,对象n插入标准输出流对象中,从而实现对象在显示器上输出。
例:求三个数平均值此处输入代码float num1 , num2 , num3;
cout<<“请输入三个数”;
cin>>num1>>num2>>num3;
cout<< num1 <<“,”<< num2 <<“,”<< num3 <<“的平均数是:”<<(num1+num2+num3)/3<< endl;
二、C++语言基础
2.1 C++数据类型
2.2 符号常量和常变量
- 符号常量 : 将具体的数值符号化,格式:
#define 符号常量名 数值
- 常变量 : 是在定义一个变量的基础上加上const修饰,其值不可修改。格式:
const 数据类型 符号常量名 = 数值
。
优点:节省空间。
2.3:算术运算符(/和%)
- 除(/):
①:当/运算符用于两个整数相除时,如果含有小数,将被截掉。
②:当/运算符中,至少由一个浮点数,则结果不会被截掉。 - 取余(%):
要求:两个操作数必须是整数或字符型。
2.4 ++、–运算符(++为例)
可分为前置++和后置++(++i、i++)
int i=0,j=0,k=0;
j=i++;
k=++i;
cout<<i<<","<<j<<","<<k<<endl;
输出结果:2,0,2
代码分析:不管前置++和后置++都会对该值加1,但是j=i++是先将i的值赋予j再i=i+1。而k=++i是先i=i+1,再将i的值赋予k。
2.5 其他运算符
-
条件运算符 格式:d1?d2:d3
例:a = (x>y? 12 : 10.0)
//若x>y,将12赋给a,否则a=10.0 -
求字节运算符 sizeof
格式:sizeof(类型说明符|变量名|常量) -
取地址运算符 &
取变量在内存中被分配的内存地址值。
2.6 循环语句
-
while
格式:while(条件表达式) 语句
-
do while
格式:do 语句 while(条件表达式);
-
for
格式:for(表达式1;表达式2;表达式3) 语句
执行顺序:表达式1 —— 表达式2 —— 语句 —— 表达式3 —— 表达式2 —— 语句 —— 表达式3 …
跳出某一次循环搭配continue使用。
跳出当前循环体搭配break使用。
2.7 函数
函数是一个命名的程序代码块,常用于使用次数多,代码固定的场合。在一个项目中可以有多个函数,但只能有一个主函数。
在函数使用前,必须先定义函数,主函数不用。不允许在函数内定义另一个函数,但可以调用另一个函数或者函数本身。
例:计算n!
int fac(int n){
int t;
if(n==1) t = 1;
else{
t = n * fac(n-1);
}
return t;
}
执行过程:t = n * fac(n-1) fac(n-1)=(n-1)*fac(n-2) … fac(2) = 2 * fac(1)=2 * 1 = 2;
则:t = n * (n-1) * (n-2) * … * 2 * 1 = n!。
三、 构造数据类型
构造数据类型也称为自定义数据类型,是用基本数据类型构造的用户自定义数据类型,包括枚举、数组、指针、字符串、引用类型、构造和联合。
3.1 枚举
如果一个变量只有几种可能的取值,可以使用枚举类型来定义。“枚举”就是指将变量的取值一一列举出来。
格式:
enum 枚举类型名 {枚举常量1, 枚举常量2, ... ,枚举常量n};
当没有给枚举常量指定值时,其值依次默认为0、1、2、3、…,后一个值将是前一个值加1。
常用枚举命名方法:
1:
enum e_sex {male,fmale};
e_sex sex;
2:
enum e_sex {male,fmale} sex;
3:
enum {male,fmale} sex;
3.2 数组
数组是一组在内存中依次连续存放的、具有通一类型的数据变量所组成的集合体。其中每个变量称为数组元素,它们属于通一种数据类型。
-
数组的初始化:
数据类型 数组名[常量表达式] = {初值1,初值2,… ,初值n};
对于整数类型的数组,如果没有对数组进行初始化,则元素为任意值,一般可以这样定义,是全部元素为0:int a[常量表达式] = {0};
-
字符串数组
初始化方式:
①:char s1[ ] = {‘H’,‘E’,‘L’,‘L’,‘0’,‘\0’};
②:char s2[ ] = “HELLO”;
几个常用的字符与字符串处理函数:
①:strlen(字符串):返回字符串的长度,不包括\0
②:strcmp(s1,s2):比较两个字符串的大小,从左往右依次比较对应字符的ASCII码。大于、小于、等于依次返回1、-1、0.
③:strcpy(s1,s2):将s2复制到s1所指的存储空间中,并返回s1,s1被修改。
④:strcat(s1,s2):将s2连接到s1所指的字符串之后的存储空间中,并返回s1的值,s1被修改。
3.3 指针
在定义了一个变量后,内存中将会划出一块区域来用于存放该变量的数据。该区域由若干个存储单元组成,每个存储单元都有各自的编号,称为地址。
指针就是一种用于存放内存单元地址的变量类型。
为了防止指针指向存有重要的数据或程序代码的内存单元,在定义时应该对其进行初始化。int* p = 0; 或 int* p = NULL ;
看看下列两段代码,主函数调用会有什么不同
函数1:
void swap1(int a,int b){
int k;
k = a;
a = b;
b = k;
}
函数2
void swap2(int* a,int* b){
int k ;
k = *a;
*a = *b;
*b = k;
}
当主函数执行swap1(a,b);时,输出的结果并不会被交换a,b的值,因为在主函数调用swap1函数时,会自动拷贝a和b,并将其放在临时的内存空间中,一旦调用结束,系统会自动释放空间,原本的数据并不会被修改。
当主函数调用swap2(&a,&b);输出的结果是a,b的值会被交换。因为该调用是将a和b的地址作为参数,在其对应的地址上再将a,b的值进行修改,所以即使调用结束,a和b的值也不会恢复。
指针与数组
指针的加减运算的特点使指针操作符特别适合处理存储再一段连续内存空间中的通类型数据,比如数组。
int a[] = {1,2,3,4 .... };
int* p = NULL;
p = a + i;
cout<<*p<<endl;
输出结果为:a[i]的值。
指针数组与数组指针
- 指针数组:以指针变量为元素的数组,每个元素都为同一类型的指针变量。
格式:类型名 * 数组名[下表表达式]
例:
int* p_a[10];
int a[] = {1,2,3,4,5};
for(int i = 0;i<5;i++){
p_a[i] = &a[i];
}
- 数组指针:是指向数组的指针。
格式:类型名( * 指针名)[下表表达式];
例:
int( * a_p)[5];
int a[5];
a_p = &a;
3.4 动态内存分配
在处理数据相同的大量数据时,比如可以用数组来存储。在申明时必须指定其长度,但有时我们很难确定数组里面元素的个数,超出就会越界,否则又会浪费内存空间。
在C/C++中,可以使用动态内存分配来保证程序在运行时根据实际需要申请内存,使用结束后再将其释放。
- new 运算
格式:指针变量 = new 类型名 (初值列表);
对数组来说:指针变量 = new 类型名 [下标表达式];
对结构体来说:指针变量 = new 类型名 {成员1,成员2 ...};
- delete 运算
当程序不再需要由new分配的内存空间时,可以用delete释放这些空间。
格式:delete 指针变量名;
对数组来说:delete [] 指针变量名;
3.5 指针常量和常量指针
一个指针可以操作两个对象,一个是指针值(即地址),一个是通过指针间接访问的变量的值,所以指针可以分为指针常量于常量指针。
1、指针常量:就是指针值不能被修改。格式如下:
数据类型 * const 指针名 = 变量名;
例:
int a = 2;
int b = 3;
int * const p = &a;
*p = b;
cout<<"a = "<<a;
输出结果为a = 3。
指针的指针值不能被修改即不能p = &b;但是可以对指针指向变量的值进行修改。
2、常量指针:就是指向对象的值不能被修改。格式如下:
const 数据类型 * 指针变量 = 变量名;
例:
int a = 2;
int b = 3;
const int* p = &a;
p = &b;
可以改变指针值。
3.5 引用
引用是已存在变量的别名,通过引用,我们可以间接访问变量。引用的主要用途是描述函数的参数和返回值,特别是传递较大的数据变量。格式如下:
数据类型 & 引用变量名 = 变量名;
当定义了一个引用变量后,与指针不同,系统并没有为它分配内存空间不占用新的地址,节省了内存,引用与被引用变量具有相同的地址,即同一个内存空间。对引用变量值进行修改就是对被引用变量的修改。
例:
int x = 3;
int& refx = x;
refx = 5;
cout<<x;
输出结果为5。
对上面交换函数也可使用引用:
void swap3(int& x,int& y){
int k = 0;
k = x;
x = y;
y = k;
}
四 类与对象
4.1 类的定义
格式:
class 类名
{
pbulic:
公有数据成员或共有函数成员的定义;
protected:
保护数据成员或保护函数成员的定义;
private:
私有数据成员或私有函数成员的定义;
};
-
public、protected、private为存取控制属性,如果前面没有标明访问权限,默认为private;结构体默认为public权限。
-
类的成员有数据成员和函数成员两类。数据成员一般用来描述该类对象的静态属性,称为属性;函数成员用来描述类行为或动态属性,称为方法。
4.2 成员函数的定义
成员函数既可以在类中定义,也可在类外定义。在类中定义的成员函数为内联函数。
在类外定义函数体可以使成员函数一目了然,避免在各个函数实现的一大堆代码中查找函数的定义。
class Student {
pbulic:
int age;
int num;
char name;
void print_name();
};
void Student::print_name(){
......;
}
::是类的作用域分辨符,用在此处,放在类名后、成员函数前,表明后面的成员函数属于前面的那个类。
4.2 this 指针
this 指针指向当前对象,表示当前对象的地址:
H = h, M = m, S = s;
this -> H = h, this -> M = m, this -> S = s;
(* this).H = h, (* this).M = m, (* this).S = s;
相互等效,一般不直接使用this指针来引用对象成员。
特别地:
void Student::set_age(int age){
this -> age = age;
}
当数据成员与成员函数中形参名相同时,需要用this指针表明数据成员。
- this指针不是调用对象的名称,而是指向调用对象的指针的名称。
- this的值不能改变,它总是指向当前调用对象。
4.3 构造函数与析构函数
构造函数:
在类对象的初始化当中,类无法像普通变量一样直接赋值,如Student student("zhangsan",18,1)
。这种语法是错误的。因为类对象比较复杂,而且由于类的封装性,它不允许在类的非成员函数中直接访问类对象的私有和保护数据成员。这样对类对象数据的初始化工作就落到了类对象的成员函数身上,因为它们可以访问类对象的私有和保护数据成员。C++为用户提供了专门用于对象初始化的函数——构造函数。
构造函数是与类名相同的在建立对象时自动调用的函数。若定义类时,没有定义构造函数,那么编译系统就会生成一个默认形式的隐含构造函数,但这个构造函数体为空,没有任何功能。
构造函数可以重载,可以定义多个构造函数,在建立对象时会根据参数来调用相应的构造函数。
析构函数:
析构函数也称拆构函数,是在对象消失之前的瞬间自动调用的函数,释放构造函数时所申请的内存单元。
析构函数形式:~构造函数名();
特点:
- 析构函数没有任何参数,不能被重载,但可以是虚函数,一个类只能有一个析构函数。
- 析构函数没有返回值。
- 析构函数名在类名前加上一个逻辑非运算符“~”,以区别构造函数。
- 析构函数一般由用户自己定义,在对象消失时由系统自动调用,如果用户没有定义析构函数,那么系统将自动生成一个不做任何事的默认析构函数。
5.如果在构造函数时使用了new运算符,申请了一块动态空间,那么根据new与delete要成对使用原则,delete释放空间的运算应放在析构函数中。
4.4 静态成员
C++提供的静态成员,用于解决同一个类的不同对象之间的数据成员和函数的共享问题。比如一个学校内的有许多学生,但只有一个公共的校长。所以如果将该校长当作某一个对象的成员变量就显得不太合适,这时就需要将其定义为静态成员变量。
类的静态成员分为静态数据成员和静态函数成员。
- 静态数据成员:
静态数据成员采用static关键字定义,属于类属性,每个类只有一个副本,由该类的所有对象共同维护和使用,从而实现了同类的不同对象之间的数据共享。(类的普通数据成员在类的每一个对象中都拥有一个副本,也就是每个对象的同名数据成员可以分别存储不同的数值。)
静态数据成员需要在类内声明和类外初始化。
类内声明:static 数据类型 静态数据成员名;
类外初始化:数据类型 类名::静态数据成员名 = 初始值;
- 静态成员函数:
静态成员函数用来存取类的静态成员,是该类的所有对象共享是成员函数。
静态成员函数与普通成员函数一样,可以在类内定义,也可以在类内声明,再到类外定义。在类外定义时,不能使用static关键字为前缀。
4.5 拷贝构造函数
拷贝构造函数为构造函数的一种。在建立新对象时将已存在的对象数据成员赋值给新对象时,系统会调用拷贝构造函数。如
Student stu1("zhangsan",10010,'m');
student stu2 = stu1;
这时,如果用户没有编写拷贝构造函数,系统会默认的将对象1复制给对象2,并没有给对象2,另外分配内存资源,如果对象的数据成员是指针,那么这两个指针对象实际指向同一块内存空间。这会对程序带来数据安全的隐患。比如如果对象1的数据成员需要动态的申请内存空间,再进行上述拷贝后,由于两个指针指向同一块内存空间,在进行空间释放时会在同一块内存空间释放两次,程序会出现异常。这种拷贝称为浅拷贝。
在类的对象成员有指针型时,我们必须定义一个特定的拷贝构造函数,该拷贝构造函数不仅可以对原对象和新对象之间的数据成员进行复制,而且可以为新对象分配单独的内存资源,这就是深拷贝构造函数。
如:
Student(Student &obj){
int o_len = strlen(obj,name);
p_name = new char [o_lent + 1];
memset(p_name , 0 , o_len + 1);
strcpy_s(p_name , o_lent + 1 , obj.name);
拷贝构造函数中的obj,用来指代原对象。
五、继承与派生
5.1 继承的概念
在C++中,用户可以自己利用已有的类定义新类(不用重复造轮子),新类讲拥有原类的全部特性,原有类被称为基类或父类,新产生的类称为派生类或子类。派生类拥有基类的特性称为继承,由基类产生派生类的过程称为派生。
例如:在一个教育系统中,人可以分为学生、老师、教职工等。如果每一种人都含有姓名,性别,年龄这种基本属性,如果在每个类中都重复定义这些属性将会造成整个程序代码十分冗余,并且维护起来会十分麻烦。这时就可以使用继承。
class Cperson{
public:
char *p_name;
int m_age;
char m_sex;
Cperson(){};
~Cperson(){};
};
class Cstudent : public Person{
public:
int n_sID;
};
class Cteacher : public Person{
public:
ing n_tID;
};
在上述代码中,首先定义了一个人类为基类,含有名字、年龄、性别数据成员,派生类:学生、老师以公有继承的方式继承原类中的数据成员,并添加了自己的数据成员学号。
5.2 继承的方式
继承的方式共有三种:公有继承、保护继承、私有继承。
各继承的特点如下:
父类 共有继承(public) 子类
公有成员(public) 共有成员(public)
保护成员(protected) 保护成员(protected)
私有成员(private) 不可见
父类 保护继承(protected) 子类
公有成员(public) 保护成员(protected)
保护成员(protected) 保护成员(protected)
私有成员(private) 不可见
父类 私有继承(private) 子类
公有成员(public) 私有成员(private)
保护成员(protected) 私有成员(private)
私有成员(private) 不可见
5.3 派生类的构造与析构
派生类构造函数:
子类可以继承父类所有的成员变量和成员函数,但是不能继承父类的构造函数。因此,在创建子类的时候,为了初始化父类继承来的数据成员,系统需要调用父类的构造函数。
调用规则为:
如果子类没有显示的调用父类的构造函数,那么系统会默认调用父类的无参构造函数。
如果父类只提供了有参的构造函数,那么子类在默认情况下调用父类的无参构造函数时就会报错。
如果子类调用父类带参的构造函数,需要用初始化父类的成员对象的方式。如:Cstudent():Cperson("zhangsan"){}
父类子类构造函数调用顺序:
如果子类中也有构造函数,那么在创建子类对象时,构造函数的调用顺序为:先调用父类构造函数,再调用子类的构造函数。
派生类析构函数:
跟造函数一样,父类的析构函数也不能继承给子类,也是通过派生子类的析构函数去调用父类的析构函数。在执行子类的析构函数时,系统会自动调用父类的析构函数和子对象的析构函数,对父类和子对象进行清理。
调用顺序:
与构造函数相反,先执行子类的析构函数,再调用父类的析构函数。
六、 多态性
6.1 多态性的概述
多态是指同样的消息被不同类型的对象接受时导致完全不同的行为。所谓消息就是指对类的成员函数的调用,不同的行为指不同的实现,也就是调用了不同的函数。(就是被特殊类继承之后,可以具有不同的数据类型或表现出不同的行为)
6.2 虚函数
当基类指针指向派生类实例时,可以通过基类指针调用派生类数据成员,但是如果通过基类指针调用派生类的成员函数时,结果则是调用的是基类的成员函数。
例:
class CPerson{
public:
int c_age;
CPerson(){
c_age = 1;
}
~CPerson(){}
void ceat(){
cout<<"人在吃饭。"<<endl;
}
};
class CStudent : public CPerson{
public:
CStudent(){
c_age = 20;
}
~CStudent(){}
void ceat(){
cout<<"学生在吃饭。"<<endl;
}
};
int mian(){
CPerson * per;
CStudent std;
per = &std;
cout<<per->m_age<<end;
per->ceat();
return0;
}
输出结果为:
20
人在吃饭。
如果我们在基类的c_eat()前面加上virtual,即virtual void ceat()
,后,输出的结果就是:
20
学生在吃饭。
虚函数使得在使用基类指针调用相应派生类的函数时,使其不再限制在基类函数本身,可以调用派生类中被重写的函数中。