程序设计与算法(三)郭炜
第一章:从C到C++
引用的概念:
- 定义引用时一定要将其初始化成引用某个变量。
- 初始化后,他就一直引用该变量,不会再引用别的变量了。
- 引用只能引用变量,不能引用常量和表达式
引用作为函数的返回值,然后被引用的函数可以放在等号的左面。
常引用:不能通过常引用去修改其引用的内容
- const T&和T&是不同的类型,T&类型的引用或T类型的变量可以用来初始化const T&类型的引用,反之不行
- const和define都可以用来定义常量,建议用const,因为有类型,方便检查
- 可以用来定义常量指针:1.不可以通过常量指针修改其指向的内容。 2.不能把常量指针赋值给非常量指针,反过来可以。3.作为常量指针的时候可以避免函数内部改变所指地方的内容
new运算符实现动态内存分配:P = new T[N];
- T是任意类型名,P是类型为T*的指针,返回的是T *
- N:要分配的数组元素的个数,可以是整型表达式
- 用new动态分配的内存空间,要用delete运算符进行释放
- 用delete释放动态分配的数组,要加[],如果不加的话不能完全释放存储空间
内联函数
- 函数调用有时间开销,函数如果执行速度快。相比较之下开销就会显得很大,因此使用内联函数机制,内联函数是将整个函数的代码插入到调用语句处,而不会产生调用函数的语句。
- 在函数前面加”inline”关键字,可以定义内联函数。
函数重载
- 一个或者多个函数,名字相同,然而参数个数或参数类型不相同,这叫做函数的重载。
- 编译器根据实参的个数和类型判断应该调用哪一个函数
函数的缺省参数:定义函数的时候可以让最右面的连续若干个参数有缺省值,在调用函数的时候,相应位置就可以不写参数,参数就是缺省值
- 缺省参数的目的在于提高程序的可扩充性
面向对象的程序设计:
- “抽象”:类里面的函数
- “封装”:数据类型和函数封装成类
- “继承”:继承父类的变量和函数
- “多态”:调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
类和对象的基本概念
- 类定义出来的变量,也称为类的实例,就是我们所说的对象。
- 对象所占用的内存空间的大小,等于所有成员变量的大小之和。成员函数所占用的存储空间不算在里面。
- 每个对象都有自己的存储空间,一个对象的某个成员变量改变了,不会影响到另一个函数。
- 指针用的是->,变量调用成员用.,引用名也是通过.的方式调用变量或者函数。
第二章:类和对象基础
- 类的成员函数和类的定义分开写:X::说明后面的函数是X类的成员函数,而非普通函数。一定要通过对象或对象的指针或对象的引用才能调用。
- private:私有成员,只能在成员函数内访问。public:公有成员,可以在任何地方访问。protected:保护成员。
- 如果没有上述关键字,则缺省地被认为是私有成员。
类成员的可访问范围
- 在类的成员函数内部,能够访问:当前对象的全部属性,函数;同类其他对象的全部属性,函数。
- 在类的成员函数以外的地方,只能够访问该类对象的公有成员。
构造函数
- 成员函数的一种
- 名字与类型相同,可以有参数,不能有返回值(void也不行)
- 作用是对对象进行初始化,如给成员变量赋初值
- 如果定义的时候没写构造函数,则编译器生成一个默认的无参数的构造函数,默认构造参数无参数,不做任何操作。
- 如果定义了构造函数,则编译器不生成默认的无参数的构造函数
- 对象生成时构造函数自动被调用。对象一旦生成,就再也不能再其上执行构造函数。
- 一个类可以有多个构造函数。
- 构造函数可以不用再专门写初始化函数,不用担心忘记调用初始化函数,有事对象没被初始化就使用,会导致程序出错。
- 构造函数可以重载
class Test{ public: Test(int n){} Test(int n, int m){} Test(){} }; Test array1[3]={1, Test(1, 2)};//三个元素分别用123初始化 Test array2[3]={Test(2, 3), Test(1, 2), 1};//三个元素分别用221初始化 Test * pArray[3]={new Test(4), new Test(1, 2)}//两个元素分别用12初始化,由于初始化的是指针,指针是不会调用构造函数的,所以只有new出来的两个对象才是调用了构造函数的,所以分别用了12初始化。
复制构造函数
- 只有一个参数,即对同类对象的引用。
- 形如 X::X(X&)或X::X(const X&),两者二选一,后者能以常量对象作为参数
- 如果没有定义复制构造函数,那么编译器生成默认复制构造函数。默认的复制构造函数完成复制功能。
起作用的三种情况:
- 当用一个对象去初始化同类的另一个对象时。
Complex c2(c1); Complex c2 = c1;
- 如果函数有一个参数是类A的对象,那么该函数被调用时,类A的复制构造函数将被调用。
- 如果函数的返回值是类A的对象时,则函数返回时,A的复制构造函被调用
对象间赋值并不导致复制构造函数被调用
常量引用参数的使用:
void fun(CMyclass obj_){
cout<<"fun"<<endl;
}
- 调用时生成形参会引发复制构造函数调用,开销比较大
- 考虑使用CM有class &引用类型作为参数
- 如果希望实参的值在函数中不应被改变,那么可以加上const关键字
void fun(const CMyclass & obj){
cout<<"fun"<<endl;
//函数中任何试图改变object值的语法都将是变成非法
}
类型转换构造函数
- 定义转换构造函数的目的是实现类型的自动转换。
- 只有一个参数,而且不是复制构造函数的析构函数,一般就可以看做是转换构造函数
- 当需要的时候,编译系统会自动调用转换构造函数,建立一个无名的临时对象
析构函数
- 名字与类名相同,前面加’~’,没有参数和返回值,一个类最多只能有一个析构函数
- 析构函数对象消亡时即自动被调用,可以定义析构函数来在对象消亡前做善后工作,比如释放分配的空间等
- 如果定义类时没有写析构函数,则编译器生成缺省析构函数。缺省析构函数什么也不做
- 对象数组生命期结束时,对象数组的每个元素的析构函数都会被调用
- delete运算导致析构函数调用
- 若new一个对象数组,那么用delete释放时应该写[],否则只delete一个对象(调用一次析构函数)
- 析构函数在对象作为函数返回值后被调用
测试案例
Demo d1(1); // id=1 constructed 全局对象在main之前就已经定义了,所以先调用
void Func()
{
static Demo d2(2); //这里面的d2是静态对象,所以要到main结束才会销毁
Demo d3(3);
cout<<"func"<<endl;
}
int main(){
Demo d4(4);
d4 = 6; //6会被类型转换构造函数转换为一个临时对象,到时id=6 construction,临时对象赋值给d4后就会被销毁,引发id = 6 destructed
cout<<"main"<<endl;
{
Demo d5(5); //在大括号里面的语句在括号结束后就会被销毁,这里id=5的构造之后就是id=5的析构
}
Func();
cout<<"main ends"<<endl;
return 0;
}
//程序结束后,全局对象和静态对象被销毁,一般析构的顺序为谁先构造谁后析构。
第三章:类和对象提高
this指针: 其作用就是指向成员函数所作用的对象。
- 静态成员函数中不能使用this指针,因为静态成员函数并不具体作用于某个对象,因此,静态成员函数的真实的参数的个数,就是程序中写出的参数个数。
静态成员:
- 静态成员:在说明前面加了static关键字的成员
- 普通成员变量每个对象有各自的一份,而静态成员变量一共就一份,为所有对象共享。
- sizeof运算符不会计算静态成员变量。
- 普通成员函数必须具体作用于某个对象,而静态成员函数并不具体作用于某个对象。
- 静态成员不需要对象就可以访问
- 在静态成员函数中,不能访问非静态成员函数,也不能调用非静态成员函数。
成员对象和封闭类:
- 任何生成封闭类对象的语句,都要让编译器明白,对象中的成员对象,是如何初始化的。
- 具体的做法:通过封闭类的构造函数的初始化列表。
- 封闭类对象生成时,先执行所有对象成员的构造函数,然后才执行封闭类的构造函数。
- 对象成员的构造函数调用次序和对象成员在类中的说明次序一致,与他们在成员初始化列表中出现的次序无关。
- 当封闭类的对象消亡时,先执行封闭类的析构函数,然后再执行成员对象的析构函数。次序和析构函数的调用次序相反。
常量对象,常量成员函数和常引用:
- 常量对象:如果不希望某个对象的值被改变,则定义该对象的时候可以在前面加const关键字。
- 在类的成员函数说明后面可以加const关键字,则该成员函数成为常量成员函数
- 常量成员函数执行期间不应修改其所作用的对象。因此,在常量成员函数中不能修改成员变量的值(静态成员变量除外),也不能调用同类的非常量成员函数(静态成员函数除外)
- 两个成员函数,名字和参数表都一样,但是一个是const,一个不是,算重载。
- 引用前面可以加const关键字,成为常引用。不能通过常引用,修改其引用的常量。
友元:
- 友元函数:一个类的友元函数可以访问该类的私有成员。在类中声明友元函数,前面加friend。
- 友元类:friend class X;如果A是B的友元类,那么A的成员函数可以访问B的私有成员。
- 被声明的友元函数或者友元类可以访问原先类里面的变量和成员函数。
- 友元类之间的关系不能传递,不能继承。
第四章:运算符重载
运算符重载:目的:拓展c++中提供的运算符的适应范围,使之能作用于对象。
- 运算符重载的实质是函数重载
- 可以重载为普通函数,也可以重载为成员函数
- 把含运算符的表达式转换成对运算符函数的调用
- 把运算符的操作数转换成运算符函数的参数
- 运算符被多次重载时,根据实参的类型决定调用哪几个运算符函数
- 重载为成员函数时,参数个数为运算符目数减一
- 重载为普通函数时,参数个数为运算符目数
赋值运算符重载:
- 赋值运算符“=”只能重载为成员函数
- 浅拷贝和深拷贝
- 一般情况下,将运算符重载为类的成员函数,是较好的选择
- 但有时,重载为成员函数不能满足使用要求,重载为普通函数,又不能访问类的私有成员,所以需要将运算符重载为友元
流插入运算符的重载:
ostream & operator<<(ostream & o, const CStudent & s){
o<<s.nAge;
return o;
}
重载类型转换运算符:
operator double(){return real;}//重载强制类型转换运算符double
- 可以显示转换,也可以隐式转换(double A = B + complex)
自增自减运算符的重载:
CDemo & CDemo::operator++(){
++ n;
return * this;
}//++s即为s.operator++()
CDemo :: operator++(int k){
CDemo tmp(*this);
n ++;
return tmp;
}//++s即为s.operator++(0)
CDemo & operator--(CDemo & d){
d.n --;
return d;
}//--s即为operator--(s)
CDemo operator--(CDemo &d, int){
CDemo tmp(d);
d.n --;
return tmp;
}//s--即为operator--(s, 0)
运算符重载的注意事项:
- C++不允许定义新的运算符
- 重载后运算符的含义应该符合日常习惯
- 运算符重载不改变运算符的优先级
- 以下运算符不能被重载:” . “ “ .* “ “ :: “ “ ?: “ sizeof;
- 重载运算符(), [], ->或者赋值运算符=时,运算符重载函数必须声明为类的成员函数
第五章:继承和派生
- 派生类是通过对基类进行修改和扩充得到的,在派生类中,可以扩充新的成员变量和成员函数。
- 派生类一经定义后,可以独立使用,不依赖于基类。
- 派生类拥有基类的全部成员函数和成员变量,不论是private,protected,public。在派生类的各个成员函数中,不能访问基类中的private成员。
- 派生类对象的内存空间:派生类对象的体积,等于基类对象的体积,再加上派生类对象自己的成员变量的体积。在派生类对象中,包含着基类对象,而且基类对象的存储位置位于派生类对象新增的成员变量之前。
继承关系和复合关系:
- 继承:“是”关系。基类A,B是基类A的派生类,逻辑上要求:“一个B对象也是一个A对象”
- 复合:“有”关系,类C中“有”成员变量k,k是类D的对象,则C和D是复合关系。一般逻辑上要求:“D对象是C对象的固有属性或组成部分”
class CPoint{ double x,y; }; class CCircle{ double r; CPoint center; };//复合关系
覆盖和保护对象
- 派生类可以定义一个和基类成员同名的成员,这叫覆盖。在派生类中访问这类成员时,缺省的情况是访问派生类中定义的成员。要在派生类中访问由基类定义的同名成员时,要使用作用域符号::
- 一般来说,基类和派生类不定义同名成员变量
基类成员函数总结
- 基类的private成员:可以被下列函数访问:(1):基类的成员函数(2):基类的友元函数
- 基类的public成员:可以被下列函数访问:(1):基类的成员函数(2):基类的友元函数 (3):派生类的成员函数 (4):派生类的友元函数 (5)其他的函数
- 基类的protected成员:可以被下列函数访问:(1):基类的成员函数(2):基类的友元函数(3):派生类的成员函数可以访问当前对象的基类的保护成员
派生类的构造函数
FlyBug::FlyBug (int legs, int color, int wings): Bug(legs, color){
nWings = wings;
}
- 在创建派生类的对象时,需要调用基类的构造函数:初始化派生类对象中从基类继承的成员。在执行一个派生类的构造函数之前,总是先执行基类的构造函数。
- 派生类的析构函数被执行时,执行完派生类的析构函数后,自动调用基类的析构函数。
- 构造析构顺序:在创建派生类的对象时:(1)先执行基类的构造函数,用以初始化派生类对象中从基类继承的成员;(2)再执行成员对象类的构造函数,用以初始化派生类对象中的成员对象。(3)最后执行派生类自己的构造函数。析构函数的调用顺序与构造函数的调用顺序相反。
public继承的赋值兼容规则
class dervied : public base{};
(1)派生类的对象可以赋值给基类对象 (2)派生类对象可以初始化基类引用 (3)派生类对象的地址可以赋值给基类指针
直接基类与间接基类
- 在声明派生类时,只需要列出它的直接基类,派生类沿着类的层次自动向上继承它的间接基类。
- 派生类的成员包括:派生自己定义的成员,直接基类中的所有成员, 所有间接基类的全部成员
第六章:多态
虚函数:在类的定义中,前面有virtual关键字的成员函数就是虚函数。
- virtual关键字只用在类定义里的函数声明中,写函数体时不用
- 构造函数和静态成员函数不能是虚函数
- 派生类的指针可以赋给基类指针
- 通过基类指针调用基类和派生类中的同名虚函数时:(1)若该指针指向一个派生类的对象,那么被调用是基类的虚函数;若该指针指向一个派生类的对象,那么被调用的是派生类的虚函数
- 派生类的对象可以赋给基类使用
- 通过基类引用调用基类和派生类中的同名虚函数时:(1)若该引用引用的是一个基类的对象,那么被调用的是基类的虚函数(2)若该引用引用的是一个派生类的对象,那么被调用的是派生类的虚函数
- 多态的作用:在面向对象的程序设计中使用多态,能够增强程序的可扩充性,即程序需要修改或增加功能的时候,需要改动和增加的代码较少。
- 纯虚函数
virtual double Area()=0;//纯虚函数
- 在非构造函数,非析构函数的成员函数中调用虚函数,是多态!!!
- 在构造函数和析构函数中调用虚函数,不是多态。编译时即可确定,调用的函数是自己的类或基类中定义的函数,不会等到运行时才决定调用自己的还是派生类的函数。
- 派生类和基类中虚函数同名同参数表的函数,不加virtual也自动成为虚函数
- 多态的关键在于通过基类指针或引用调用一个虚函数时,编译时不确定到底调用的是基类还是派生类的函数,运行时才确定,这叫动态联编
多态的实现原理
- 每一个有虚函数的类(或有虚函数的类的派生类),都有一个虚函数表,该类的任何对象中都放着虚函数表的指针。虚函数表中列出了该类的虚函数地址。多出来的4个字节就是用来放虚函数表的地址的
- 多态的函数调用语句被编译成一系列根据基类指针所指向的(或基类引用所引用的)对象中存放的虚函数表的地址,在虚函数表中查找虚函数地址,并调用虚函数的指令
- 多态在运行的时候会有额外的时间和空间上的开销,空间上的开销指的是多出来的存放虚函数表的4个字节,时间的开销包括查虚函数表的过程
虚析构函数,纯虚函数和抽象类
- 通过基类的指针删除派生类对象时,通常情况下只调用基类的析构函数,但是,删除一个派生类的对象时,应该先调用派生类的析构函数,然后调用基类的析构函数。
- 解决方法:把基类的析构函数声明为virtual
- 派生类的析构函数可以virtual不进行声明
- 通过基类的指针删除派生类对象时,首先调用派生类的析构函数,然后调用基类的析构函数
- 一般来说,一个类如果定义了虚函数,则应该将析构函数也定义成虚函数,或者,一个类打算作为基类使用,也应该将虚构函数定义成虚函数
- 纯虚函数:没有函数体的虚函数。
virtual double Area()=0;//纯虚函数
- 包含纯虚函数的类叫做抽象类
- 抽象类只能作为基类来派生新类使用,不能创建抽象类的对象
- 抽象类的指针和引用可以指向由抽象类派生出来的类的对象
- 在抽象类的成员函数内可以调用纯虚函数,但是在构造函数或析构函数内部不能调用纯虚函数
- 如果一个类从抽象类派生而来,那么当且仅当它实现了基类中的所有纯虚函数,它才能成为非抽象类
第七章:输入输出和模板
- cin对应于标准输入流,用于从键盘读取数据,也可以被重定向为从文件中读取数据
- cout对应于标准输出流,用于向屏幕输出数据,也可以被重定向为向文件写入数据
- cerr对应于标准错误输出流,用于向屏幕输出出错信息
- clog对应于标准错误输出流,用于向屏幕输出出错信息
- cerr和clog的区别在于cerr不使用缓冲区,直接向显示器输出信息;
- 输出重定向:
freopen("test.txt", "w", stdout);//将标准输出重定向到test.txt文件 if(y==0) cerr<<"error"<<endl; else cout<<x/y; return 0;
- 输入重定向
freopen("t.txt", "r", stdin); cin>>f>>n; cout<<f<<", "<<n<<endl; return 0;
- 判断输入流结束,如果是从文件输入,比如前面有freopen(“some.txt”, “r”, stdin);那么,读到文件尾部,输入流就算结束。如果从键盘输入,则在单独一行输入Ctrl+Z代表输入流结束。
int x; while(cin>>x){ ... } return 0;
istream类的成员函数
- bool eof();判断输入流是否结束
- int peek();返回下一个字符,但不从流中去掉
- istream & putback(char c); 将字符ch返回输入流
- istream & ignore(int nCount = 1, int delim = EOF);从流中删除最多nCount个字符,遇到EOF时结束
流操纵算子
- 整数流的基数:流操纵算子dec, oct, hex, setbase
- 浮点数的精度(precision, setprecision)
- 设置域宽(setw, width)
- 用户自定义的流操纵算子
- 使用流操纵算子需要 #include
- 流操纵算子dec, oct, hex的使用
cout<<hex<<n<<endl;//dec->十进制, oct->八进制, hex->十六进制
- 控制浮点数精度的流操纵算子setprecision
cout<<setprecision(6)<<x<<endl<<y<<endl;//是有x的输出是精度需要维持6位,y的输出还是正常效果,以科学计数法的形式出现
cout<<setiosflags(ios::fixed)<<setprecision(6)<<x<<endl<<y<<endl<<n<<endl<<m;//以小数点固定的方式输出,小数的话小数点后面保留6位,不足的补0
cout<<setiosflags(ios::fixed)<<setprecision(6)<<x<<endl<<resetiosflags(ios::fixed)<<x;//前面的x输出会以小数点固定的方式输出,后面的x会科学计数法的方式输出
- 设置域宽的流操纵算子
设置域宽(setw, width):两者功能相同,一个是成员函数,另一个是流操作算子,调用方式不同:
cin>>setw(4);或者cin.width(5);
cout<<setw(4); 或者 cout.width(5);
int w=4;
char string[10];
cin.width(5);
while(cin>>string){
cout.width(w++);
cout<<string<<endl;
cin.width(5);
}
//宽度设置有效性是一次性的,在每次读入和输出之前都要设置宽度。在输出的宽度设置的比输入进来的多的时候,要在前面打印空格。然后输入设置的宽度是5,每次输入进来4个,因为最后面有一个‘\0’
用户自定义流操纵算子:
ostream &tab(ostream &output){
return output<<"t";
}
cout<<"aa"<<tab<<"bb"<<endl;//输出aa bb
文件读写,函数模板,类模板,类模板与派生,友元和静态成员变量先不做笔记,不会的时候先去查找资料
- 第八章第九章同上
第八章:标准模板库STL(一)
容器 | 容器上的迭代器类别 |
---|---|
vector | 随机访问 |
deque | 随机访问 |
list | 双向 |
set/multiset | 双向 |
map/multimap | 双向 |
stack | 不支持迭代器 |
queue | 不支持迭代器 |
priority_queue | 不支持迭代器 |
- 有的算法,例如sort,binary_search需要通过随机访问迭代器来访问容器中的元素,那么list以及关联容器就不支持该算法。
- 双向迭代器不支持<,list没有[]成员函数
第九章:标准模板库STL(二)
- priority_queue 和队列相同,大的元素放在上面,如何判断大小可以选择不同的比较器
第十章:C++11新特征和C++高级主题
- 统一的初始化方法
- 成员变量默认初始值
- auto关键字(用于定义变量,编译器可以自动判断变量的类型,需要设置一个初值知道是什么类型的变量)
- decltype 求表达式的类型,可以用来定义一个和已知变量相同类型的变量(当把已知变量用括号括起来的时候,求出来的变量就是已知变量类型的引用)
-
智能指针shared_ptr: (1)头文件
(2)通过shared_ptr的构造函数,可以让shared_ptr对象托管一个new运算符返回的指针,写法如下: (3)shared_ptr ptr(new T);//T可以是int, char, 类名等各种类型,以后ptr就可以像T*类型的指针一样来使用,即\*ptr就是用new动态分配的那个对象,而且不必操心释放内存的事。 (4)多个shared_ptr 对象可以同时托管一个指针,系统会维护一个托管计数。当无shared_ptr托管指针时,delete该指针 (5)shared_ptr对象不能托管指向动态分配的数组的指针,否则程序运行会出错。 (6)sp1.reset()//可以放弃shared_ptr对象原本托管的对象 - 空指针nullptr(和NULL使用基本一样,代表空指针,nullptr不能自动转换成整型)
- 基于范围的for循环
#include<iostream>
#include<vector>
using namespace std;
struct A {
int n;
A(int i):n(i){}
};
int main(){
int ary[] = {1,2,3,4,5};
for(int & e:ary) e *= 10;
for(int e:ary) cout<<e<<",";
cout<<endl;
vector<A> st(ary, ary+5);
for( auto $ it : st) it.n *= 10;
for( A it: st) cout<<it.n<<",";
return 0;
}
右值引用和move语义
- 右值:一般来说,不能取地址的表达式,就是右值,能取地址的,就是左值
- 主要目的是提高程序运行的效率,减少需要进行深拷贝的对象进行浅拷贝的次数
- move(a);作用是把右值转换为左值,适用于a是临时变量,避免深拷贝,会改变原本指向的内容,所以适用于a为临时变量
无序容器(哈希表)
- unorderedmap
正则表达式
#include<regex>
Lambda表达式
强制类型转换
static_cast
- static_cast用来进行比较“自然”和低风险的转换,比如整型和实数型,字符型之间相互转换。
- static_cast不能来在不同类型的指针之间相互转换,也不能用于整型和指针之间的互相转换,也不能用于不同类型的引用之间的转换
reinterpret_cast
- reinterpret_cast用来进行各种不同类型的指针之间的转换,不同类型的引用之间转换,以及指针和能容纳得下指针的整数类型之间的转换。转换的时候,执行的是逐个比特拷贝的操作。
const_cast
- 用来进行去除const属性的转换。将const引用转换成同类型的非const引用,将const指针转换成同类型的非const指针时用它
dynamic_cast
- dynamic_cast 专门用于将多基态类的指针或引用,强制转换为派生类的指针或引用,而且能够检查转换的安全性。对于不安全的指针转换,转换结果返回NULL指针
- dynamic_cast不能用于将非多态基类的指针或引用,强制转换为派生类的指针或引用
异常处理
- 用try,catch处理
异常的再抛出
- 如果一个函数在执行的过程中,抛出的异常在本函数内部就被catch块捕获并处理了,那么该异常就不会抛给这个函数的调用者(也称“上一层的函数”);如果异常在本函数中没被处理,就会被抛给上一层的函数