什么是程序?程序就是可以被计算机执行的用于解决问题的一系列计算机指令。
编程过程中,两种常用的编程思想:面向过程编程思想(代表语言:C语言)和面向对象编程思想(代表语言:C++、Java等)
面向过程的编程:是一种自上而下的设计方式: 使用三步表示些种编程过程为:获得用户输入的数据,对数据进行运算并做出某种决策,在屏幕显示计算结果。即:1、输入数据,2、计算,3、输出结果。
面向对象的编程:首先需要将问题分解成各个对象,从而对对象的属性和行为以及对象之间的关系进行分析。
两者的主要区别可以简单概括为: 面向过程的编程是一种直接的编程方法,是按照编程语言的思路考虑问题; 面向对象的编程是一种抽象度更高的编程方法,它的目标是使模块的抽象度高,可复用性更好。
在面向对象的编程中,有三大特征:封装、继承、多态
封装:1把变量和函数写在一个类中,实现封装。2不想暴露在外界的成员私有化 继承:子类继承父类的成员,代码复用。 多态:父类的指针或引用有多种表现形态,用父类的指针或引用统一操作各子类对象。
1、对象: Object=state+behavior(对象=状态+行为) 任何一个对象都具有两个要素:属性和行为。其中属性用于描述客观事物的静态特征,为名词。而行为用于描述事物的动态特征,为动词。 如一个人,他有姓名、性别、身高、体重等属性,有走路、说话、打手势、学习和工作等行为。又如一个银行账户,属性:用户名,密码,余额等,行为:转账,查看余额,贷款等。 2、类: 类是对客观世界中具有相同属性和行为的一组对象的抽象,它为属于该类的全部对象提供了统一的抽象描述,其内容包括属性(成员变量)和行为(队成员变量进行操作的函数)。 如:人可以作为一个类,它是世界上所有实体人如张三、李四、王五等的类型,而实体人张三、李四等则是人这个类的具体实例。
所以类和对象的关系可以总结为:类是对象的抽象类型,而对象是类的具体实例。
C语言的结构体与C++类的区别:C语言结体成员的默认访问权限为公有,而C++的成员的默认访同杈限为私有。
注意: (1)声明类时,类名首字母一般大写,如class Point{}, class Person{}等 (2)函数名书写原则:(驼峰原则)除开头外的单词的首字母不需要大写外,连接的每一个单词的首字母都应当大写,如setX()、setY()、setRadius()等; (3)对于类的成员函数的实现有两种方式,一是在类内部声明并直接实现,二是在类内部声明函数,在类外实现,但是在类外实现时需要在函数名前加作用域符号::。 (4)输入输出:在C语言中的标准输入愉出头文件为# include <stdio.h>,而在C + +中使用的是输入输出流头文件为#include <iostream.h>或#include using namespace std;建议使用后者,因为有些编译器不支持第一种的形式。 (5)在iostream中的输入和输出分别为cin和cout (6)声明的对象不能直接访问私有成员,可以通过get函数来进行访问。
1、构造函数特点 构造函数是一种比较特殊的成员函数,用于创建并初始化对象。声明对象时构造函数会被编译器自动调用。 构造函数的四个特点: (1)构造函数的访问权限必须为公有(public); (2)构造函数名和类名相同; (3)构造函数没有返回值; (4)构造函数可以带参数,用于初始化成员变量; 2、默认构造函数 默认构造函数分两种:1、构造函数不带参数;2、构造函数带参数但参数都有默认值。 Example codes: Circle();/ /默认构造函数 Circle(float a=0,float b=0,float c=0);/ /默认构造函数 Circle( float a, float b, float C);/ /普通构造函数
默认构造函数是指在声明对象时,可以不传递参数,使用空值或默认值对对象进行初始化,而普通构造函数在声明对象时必须传递参数(实参),否则构造函数的参数(形参)没有值,编译出错。
如果程序员一个构造函数都没写,编译器将提供一个空的默认构造函数,它不会对数据成员进行初始化,一旦程序员提供了构造函数,编译器就不会提供默认构造。所以,对每个类都写一个默认构造函数,是程序员的好习惯。
在同一个作用域中,可以有一组相同函数名、不同参数列表的函数,这组函数称为重载函数。 参数列表不同是指参数的个数或参数类型不同, 如:
void f(); void f(int x); void f(int x , int y);但如果仅仅是函数返回值不同,则不能是函数的重载,在同一个作用域内不能共存,因为函数调用时不知调用哪一个函数。 如:
void f(int x); Int f(int x); //error: Functions that differ only in their return type cannot be overloaded.在使用C++编程的过程当中,常常需要在构造函数中对类的成员变量进行初始化,通常的方法有两种: 第一种方法:在构造函数体内通过賦值语句初始化
Thing(int a,int b,int c){ x = a; y = b; z = c;}第二种方法:使用初始化列表形式初始化
Thing(int a,int b,int c):x(a), y(b), z(c) { }程序中为什么要使用初始化列表: (1)对于类类型的成员,使用初始化列表效率更高。 (2)有些类型的成员变量必须使用初始化列表的形式初始化,如“const成员、引用成员。 (3)在继承时,子类的构造函数中初始化父类的成员时。
也叫组合类,是指将一个对象作为另一个类的成员变量。即一个类包含另一个类的对象。
创建对象时系统会自动调用构造函数进行初始化工作,同样,销毁对象时系统也会自动调用一个函数来进行清理工作(回收对象占用的内存空间),这个函数被称为析构函数。 析构函数(Destructor)也是一种特殊的成员函数,没有返回值,不需要用户调用,而是在销毁对象时自动执行。与构造函数不同的是,析构函数的名字是在类名前面加一个“~"符号。e.g:
~ Circle();析构函数的四个特点, (1)Cannot return a value.无返回值; (2)Cannot take any parameters.无参; (3)Cannot be overloaded. 不能被重载(唯一); (4)Must be public.必须公有;
注意: 如果类中有指针成员变量,并且指针指向堆空间,那么程序员必须显式地声明析构函数,释放指针指向的堆空间,避免内存泄漏。
类的对象也可以作为函数的参数或返回值,也有传值、传指针、传引用三种。 函数传参时通过传对象的引用,是为了避免产生临时对象,并且为了避免修改形参的值,通常传const引用
const Point middle(const Point &p1, const Point&p2);函数链:middle(p,q).print();//必要条件:前一个函数的返回必须是对象
如果函数以传值形式返回一个类的对象,编译器会创建一个临时对象来保存这个值。但有些编译器会做优化。
浅拷贝: 可以把一个对象赋值给另外一个对象,对象的每个成员的值,将一对一的拷贝到新的对象。这种拷贝叫逻辑拷贝,或浅拷贝。但是,如果对象含有指针成员变量,而指针变量又指向堆上空间,将只拷贝指针成员变量本身的值,造成两个对象的指针指向同一块堆上的内存空间,删除对象时将造成 二次删除。
若不能返回指向局部变量的指针或引用。 解决方案:将局部变量改为静态局部变量或全局变量
1什么是const? const是C / C + +语言中保留的一个关键字,它用来限定一个变量是只读的,不可变的。程序中使用const可以在一定程度上提高程序的健壮性。 2 const的使用 定义const常量:常量意味着初始化完成后,其值不能再被修改。 3 const和指针 常量指针:不能通过指针修改指针所指向的变量的值。但是指针可以指向别的变量。
int a const int *P = &a, *P= 20; //错误,不能通过指针修改指向的变量的值 int b = 10; P = &b; //正确,指针可以指向别的变量·指向常量的指针(指针常量):指针常量的值不能被修改,即不能存一个新的地址,不能指向别的变量。但是可以通过指针修改它所指向的变量的值。
int a = 5; int *const p = &a; *P = 20;//正确,可以通过指针修改指向的变量的值4 const和函数:const在函数值中根据修饰位置分三种 const int func( const int a)const;
修饰返回值 const int func();不能修改返回值。 修饰函数形参 int func(const int a);函数体内不能修改形参a的值。 修饰类的成员函数 int func( ) const; 函数体内不能修改成员变量的值,增加程序的健壮性或鲁棒性。
5 const对象 const Point p; 常量对象 const对象只能调用const成员函数,不能调用普通成员函数,普通对象既可以调用const成员函数也可以调用普通成员函数。
每一个对象都隐式地包含一个指针,指向对象自身,称为this指针。 当对象调用成员函数时,会默认将对象自身传递给该函数,在函数体内不直接使用对象名,而是使用this指针,即this指针指向该对象,指向调用者。
cout<<"(*this).radius"<<(*this).radius<<endl; cout<<"this->radius"<<this->radius<<endl;cout<<“this指针的值:”<<this<<endl; 与 cout<<“主函数中声明对象c的地址:”<<&c<<endl; 相同
静态局部变量 (1)静态局部变量存储在静态存储区。 (2)静态局部变量在函数结束后内存空间不会被回收,下次使用时可以保留上一次的计算结果,这与全局变量很相似。 (3)静态局部变量如果未进行初始化,会被编译器初始化为0或NULL。
静态全局变量 (1)静态全局变量只能在它所声明的文件中使用,不能被extern关键字引用。 (2)静态全局变量与全局变景的区别:生命周期不变,作用域缩小
类的静态成员 目前我们使用的所有的数据成员和成员函数都是被某个特定的对象调用,但是一个类也可能有数据成员和成员函数属于整个类,这样的成员称之为类的静态成员。 类的静态成员变量和非静态成员变量的区别 非静态成员变量,每个对象都会为自己的非静态成员变量,在内存中保留一块空间,来存储它的值。 而对于静态成员变量,整个类只有一份拷贝,不需要每个对象都保留一份拷贝。
class Student{ int id; char *name; Static char *teacherName;};如:构造一个学生类,有学生id、name、teacherName三个属性,而我们知道每一个学生都应该有自己的id和name,但是对于teachername则不需要每一个对象都有一块空间来保存teacherName,因为圻有的学生的teacherName都一样,因此只需要在整个类中有一块空间来存储这个teacherName即可,所以将*teacherName声明为静态成员变量,既可以节省空间也可以方便修改值。
类的静态成员函数: 一个类的成员函数也可以被指定一为静态成员函数,如static int totatPerson();
类的静态成员访问: 类的静态成员可以通过类名::静态成员名直接访问(Student::teacherName),或使用对象名.静态成员(Student s1 ; s1.teacherName),因为静态成员不属于某一个特定的对象,属于整个类的所有对象共有,所以推荐使用第一种方式来访问。
类的静态成员变量必须(must)在类外使用类名+作用域进行初始化,如 char *Student::teacherName = “”;
类的静态成员变量必须在类外初始化
静态成员函数只能访问静态成员,不能访问非静态成员;而普通成员函数都可以。
友元函数可以访问一个类的私有成员,但注意元友函数并不是类的成员函数。一个类也可以作为另一个类的友元类,也就意味着这个类中的所有的成员函数是另一个类的友元函数。 示例分析: (1) middle函数为point类的友元函数,则函数体内可以直接通过对象名来访问类中的私有成员; (2)友元函数并非类的成员函数,所以其本身不受访问权限修饰符的限制; (3)Circle为point类的友元类,Circle类中所有成员函数都可以访问point类中的私有成员; (4)友元函数可以直接访问类的私有成员,提高了性能,但同时破坏了封装性。 前向声明:class类名; 告诉编译器有一个类,但此时编译器并不知道该类的具体定义。
(1)一个类A可以继承另一个类B,那么我们称类B为基类(父类),类A为派生类(子类)。 (2)子类从父类继承了所有成员,除了构造函数、析构函数、赋值运算符重载函数。 (3)子类继承父类后,子类的成员分为两部分:1、继承自父类的部分(base part);2、子类自己扩展的成员(appendent part) (4)虽然父类的私有成员被子类继承,但子类依然不能直接访问这些私有成员,子类只能通过继承自父类的公有的成员函数来访问。 (5)子类可以自己实现与父类成员函数原型相同(函数名、参数列表)的成员函数,称为覆盖或重写(overwrite) (6)当通过父类对象调用被覆盖的函数时,父类版本的函数被调用,在函数名前加Base::,如Derived d;d.Base::print();当通过子类对象调用覆盖父类的函数时,子类版本的函数被调用。
protected成员 在上述示例中,子类虽然继承了父类的私有成员b_number,但是在子类的print函数中依然不能直接访问该私有成员,子类只能通过继承之父类的公有成员函数get_number来访问。 如果父类里有protected类型的成员,在子类中可以直接访问,不需要借助父类的公有函数;但是protected类型的成员,对外界依然是隐藏的,对外就像private类型一样。 所以:子类访问父类的私有成员的两种方式: (1)通过父类的公有的成员函数 (2)将私有成员改为保护成员protected
继承的使用 继承主要是在以下情况下使用: (1)两个类之间有自然的继承关系,即一个类是另一类的特例(one class of objects is a special case of another class),比如a graduateStudent is a student,a student is a person,等等。 (2)实现代码重用(代码复用)。一个类可能需要使用其他类中定义的成员,此时可以定义一个派生类继承自该类,这样我们就不必重复编写代码。
类之间最主要的两种交互关系 (1)组合:一个类是另一个类的成员变量,一个类拥有另一个类,是Has—A,有一个的关系。 eg:自行车有一个轮子。 (2)继承:一个类是另一个类的特例,IS一A,是一个的关系。 eg:一个学生是一个人。
两个类之间是自然继承关系时,我们可以使用继承来实现代码利用,然而点、圆、圆柱体本来(Has-A)关系,应该用组合实现。三者虽然没有IS一A的关系,但是为了代码重用,也可以用继承实现。(这叫:组合关系也可用继承实现)
当父类指针或引用指向子类对象,而子类中又覆盖了父类的函数,希望用父类指针或父类引用,调用到正确版本的函数(父类指针或引用实际所指向的对象版本的函数),需要把该成员函数声明为虚函数。 (1)有了虚函数后,无需向下转型,就可以正确的用父类的指针或引用,调用到子类的函数。 (2)虚函数目的:期望父类指针(或引用),不管指向父类还是子类,在调用override时,可以反映对象的真实情况。 (3)调用虚函数时,到底调用哪个版本,是根据指针或引用实际所指向的对象的类型来确定,而不是调用者本身的类型来确定。
如果一个类有子类,则这个类(父类)的析构函数必须是虚函数,即虚析构! 如果父类的析构不是虚析构,则当(用delete)删除一个指向子类对象的父类指针时,将调用父类版本的析构函数,子类只释放了来自于父类的那部分成员变量,而子类自己扩展的成员没有被释放,造成内存泄露。
虚函数被调用的时候,到底调用那个版本,在编译的时候无法确定,只有在执行时才能确定,称为动态绑定。之前的函数调用,是在编译时就可以确定调用哪个版本的函数。 绑定使得程序可以照顾到未来增加的代码,比如创建一个新的子类,并在子类中覆盖了父类的虚函数。用之前的父类指针,依然可以正确的调用到新子类里的函数,而无需对旧有代码进行更改。
多态:用一个父类指针或引用(有多种形态),统一操作各种子类对象,也包括父类对象。 为了实现动态绑定,编译器会为每一个包含虚函数的类提供一个虚函数表,这个虚函数表被一个虚指针指向,这个类的虚函数表包含一个数组用于存放虚函数的地址,每一个指针指向了类中的虚函数。 当虚函数被调用时,编译器会使用该对象中的虚指针来查找虚函数表,然后遍历虚函数表,以查找到虚函数的指针(地址),最终找到正确版本的函数。 如果一个类有虚函数,编译器会为每一个这样的类的对象增加一个成员 虚函数表指针->虚函数表(数组:虚函数的指针)->虚函数
如果创建了一个新的子类C,会怎样? 编译器为子类C创建一个虚函数表,表里存储的是虚函数的指针; 在每个c的对象里,添加一个虚指针成员变量,虚指针指向虚函数表。每个对象都多出4个字节的内存开销,来存储虚指针。 通过这个过程,我们可以看到,为了实现虚拟功能,必须构建一个虚拟表,用于每个类具有虚拟功能。 此外,在一类具有虚函数的类中的每一个对象都有一个隐藏的成员,即虚拟指针。 所有这些对象都增加了程序使用的内存空间。同时,当一个虚拟函数被调用时,程序必须在到达该函数的代码之前进行两次寻址。
多态的代价:虚函数的调用时间比普通函数调用慢10-20%,虚函数表和虚函数指针增加了内存空间的消耗和运行时间的消耗。
(1)纯虚函数:虚函数只有声明,函数体= 0,就是一个纯虚函数,纯虚函数没有函数体,不需要实现。在子类里实现纯虚函数的具体功能。 (2)抽象基类:拥有纯虚函数的类叫做抽象类,抽象类只能作为基类,不能构建对象。因为抽象内的纯虚函数没有函数体。 (3)抽象类提供了不同种类的子类对象的一个通用接口。 (4)纯虚函数被定义在派生类中。如果派生类不重写基类的纯虚函数,则派生类也是一个抽象类。 问:子类必须实现抽象基类中的所有纯虚函数吗? 答:是/不是:子类不实现所有的纯虚函数的话,子类仍然是一个抽象类。
多重继承指一个子类有两个或者两个以上的父类。 图8-1一个子类有两个父类 图1可能产生的问题:两个父类如果有同名成员变量或函数,子类对象在调用时调用谁的? 图8-2两个父类有一个共同的祖先类 图2可能产生的问题:两个父类都会继承祖先类的成员,又将继承下来的成员派生给子类,那么子类中将有两份祖先类的成员。(菱形继承)
异常处理是计算机硬件的一种机制,用于处理软件或信息系统中出现的异常状况。
在软件开发中,异常处理机制是一种比较有效的处理系统运行时错误的方法。但异常处理没有普通函数调用速度快,所以过度的错误处理会影响应用程序运行的效率。
异常处理的目的:通过异常处理,我们可以对用户在程序中的非法操作进行控制和提示,以防程序崩溃。
注:float类型的变量不能直接用==和0.0比较,可以写成-0.000001<fx<0.000001。
注意:并不是使用了try块来包含可能出现的异常代码后,程序就不会崩溃,通常还需要两个操作: (1)满足某个条件时显示的抛出(throw)一个异常 (2)使用catch块匹配抛出的异常。
有很多函数仅仅是参数类型不同,但实现过程很类似,在c++中可以写一个通用函数,使用非实例化的参数类型,该参数在函数调用时进行实例化,这样的函数称为函数模板。
类模板可以用于表示一组类,这些类仅仅是成员变量类型不同,但是对这些成员变量的操作类似。
