目录
c++的一些基本不同
面向过程:算法+数据结构 = 程序 面向对象:算法+数据结构 = 对象 ;对象 + 通讯机制 = 程序 后缀名:cpp cc cxx 1、c++中 const修饰变量不能通过指针修改,所以在c++中常用const定义常量,而不是用define 2、编译:g++ xxx.cpp 3、有更加严格的语法检查 4、引用: int a =5; int &b = a;(这里&是引用:取别名,a的别名。一个空间两个名字)。 主要用于传参(传递自己的别名)和返回值(可以返回一个变量)时用。 1、作为参数,形参改变就是实参改变 2、作为返回值,返回的是变量本身 3、引用就是变量的别名 5、函数的重载 相同的函数名,不同的参数(类型,数目) 行为相识,逻辑不同使用重载 行为相同,逻辑相同应该用模板 6、默认参数(只能写在声明) int add(int a=1);声明 int add(int a) { ....... } 传参只能从左向右。默认参数一定从右到左。 默认参数和函数的重载的歧义。 7、空指针 c语言中NULL代表空指针。 c++中用 nullptr 带指无效指针 8、c++中增加的关键字 mutable 修饰的变量可以被更改 9、内敛函数(快) 不通过跳地址,直接展开使用,所以效率很高。 限制:逻辑简单的函数才能写成内敛函数。一般不会有复杂逻辑(比如不能有:switch for while等,一般不超过5行。) 在函数声明的位置直接实现,一般不会有函数声明。 使用: inline (函数前加). 10、开辟空间 c语言:malloc free c++ :可以用但不用(malloc ,free)。c++中不用函数,而是用运算符:new开辟(异常处理失败的情况)、 delete释放。 11、作用域 C语言:全局、局部 c++: 局部、全局、类域、名词空间 名词空间:关键字 namespace 不仅有变量名,还有函数,类等 namespace my{ int a; } int main() { int a; a= 10;(局部变量a) my::a =10;(名词空间 my中的a) } 12、全局变量 C语言可以,c++不允许: int a; int a =10; C语言、c++都不可以: int a =10; int a;
c++新增特性
1、更为严格的类型检查 2、新增变量引用 3、支持面向对象 类和对象、继承、堕胎、虚函数及RTTTI(运行时类型识别) 4、新增泛型编程 支持模板,标准模板库(STL) 5、支持异常处理 6、支持函数及运算符重载 7、支持名词空间 用于管理函数名、变量名及类
面向对象编程(oop)
oop是程序设计工程化的一种方法,软件构架的一种思想 重用性、灵活性、扩展性 面向过程:程序 = 数据结构 + 算法 面向对象: 对象 = 数据结构 + 算法 程序 = (对象+对象....)+对象间通讯机制 面向对象编程主要涉及概念: 类、对象、数据抽象、继承、动态绑定、数据封装、多态性、信息传递 对象: 自定义类型的具体 “变量” ,这个变量和结构体类似。但是区别结构体在于,在c++中类与对象还有更广泛、功能更强大的特有操作与属性。 类: 共享相同属性和方法的对象集合。可以看成一种自定义的类型 封装: 将数据和方法捆绑在一起,创造出一个新的类型的过程。将接口与实现分离的过程。 组合: 即是类之间的关系也是对象之间的关系。在这种关系中一个对象或者类包含了其他的对象和类。组合描述了“有”关系。 多态: 类型理论中的一个概念,一个名称可以表示很多不同类的对象,这些类和一个共同超类有关系。因此,这个名称表示的任何对象可以以不同的方式响应一些共同的操作集合。 动态绑定: 也称动态类型,一个对象或则表达式的类型直到运行时才确定。通常由编译器插入特殊代码来实现。与之对立的静态类型。 静态绑定: 也称静态类型,指的是一个对象或者表达式的类型在编译时确定 消息传递: 一个对象调用了另一对象的方法 方法: 也称成员函数,指对象上的操作,作为声明的一部分来定义。方法定义了可以对一个对象执行哪些操作。
1、格式
#include<iostream> //c++写程序一般都是写在类域里,而不是全局 using namespace std;//std名词空间的内容就在本文件直接可以用 int main() { cout //cout为输出,在std这个名词空间里 } #include<iostream> int main() { std::cout //cout为输出,在std这个名词空间里 }
2、输入输出
#include<iostream>
using namespace std;
int main()
{
int a = 123;
double b =1.232;
const char *p = "hello";
cout<<"a="<<a<<"b="<<b<<"p="<<p<<endl;
}
输出:a=123b=1.232p=hello
#include<iostream>
using namespace std;
int main()
{
int a = 123;
double b =1.232;
int buf[3];
cin>>a>>b>>buf[0];
cout<<"a= "<<a<<" b= "<<b<<" buf= "<<buf[0]<<endl;
}
输入:1 23.4 45
输出:a= 1 b= 23.4 buf= 45
3、类
C语言:结构体也可以实现面向对象编程,但是结构体中的数据在C语言中不止这一种方法,数据不安全,所以使用c++,专门面向对象 c++: 类代表了某一批对象的共性和特征,类是对象的抽象,而对象是类的具体实列。 类是抽象的,不占内存空间。就像c语言中定义了一种类型。对象就是根据该类型定义的变量。 类的内容:成员函数(方法)、成员变量(数据) class demo{ public://公有权限,都可以用 private://私有的,本类域内可用 protected://保护的,本类域内可用,继承时和private有区别 } 计算类的大小,只计算变量。不计算函数 定义类后,一般有几个默认函数,需要重写
类的声明与定义
如果设计一个类时,没有显示声明定义构造函数、析构函数、复制构造函数、复制运算符、地址运算符,则编译器会自动生成。 c++提供了构造函数来处理对象的初始化。 构造函数是一种特殊的成员函数,与其他成员函数不同 1、不需要用户来调用它也不能调用,而是建立对象时自动执行 2、构造函数的名字必须与类同名,而不能用户任意命名,以便编译系统能识别它并把它作为构造函数处理 3、没有返回值 4、构造函数的功能是由用户定义的,用户根据初始化要求设计函数体和函数参数 5、如果用户不设计,编译器自动生成一个
构造函数
构造函数可以重载 默认的构造: 1、普通构造,没有参数 2、拷贝构造(浅拷贝) 1、普通构造 默认:默认构造,没有参数和返回值。默认构造什么都不做, 重构一般带参,可以根据自己意愿初始化。当普通构造带指针时,需要重新开辟空间。 2、拷贝构造,默认浅拷贝 。 如果不涉及深浅问题,一般不会重写默认拷贝。 浅拷贝:系统默认,不会新开辟空间 深拷贝:重构需要开辟空间 3、移动构造,默认没有移动构造 存在临时变量时使用
析构函数
对象销毁时自动调用的函数,它可以显示调用。析构函数没有返回值,也没有函数参数,因此它不能被重载。 格式: ~类名(void) { if(str != nullptr)//判断是否是临时变量,如果不判断,会出现一个空间两次释放的问题。 delete 【】str; str = nullptr; } 使用析构函数显示调用时需要注意重复释放的问题。 可以显示调用析构函数。一般的,不需要进行析构函数的显示调用,也会有特殊需求:譬如对象是静态时,存在堆区分配,当程序未结束时,需要释放堆,这个时候就可以显示调用析构函数来完成。如果显示调用了析构函数,此时析构函数和普通函数是一样的,并不会造成对象被销毁。
this指针
this指针是一个特殊的指针,指向对象自身的首地址。每个类对象的成员函数都有一个this指针,指向调用对象,如果要引用整个对象则 *this。this指针仅能在类内部使用。
关键字:static
C语言的功能,c++也支持; 可以将类的成员声明成静态的(关键字static修饰的成员函数或成员变量),通过类名和作用域解析运算符来调用静态成员。 静态成员没有this指针 静态成员不和具体对象关联,也不能直接访问类的其他成员。 1、修饰变量 全局变量:生命周期不变,作用域被限制在本文件 修饰局部变量:延长生命周期,作用域不变 2、修饰函数 生命周期不变,作用域限制在本文件。 3、修饰类相关 1、修饰成员变量 特点: 静态成员不和具体对象关联,没有对象生产,静态成员也存在;并且,静态成员也不属于类,只是给它加了个类域,即作用域有限制。 多个对象共用一个静态。 (主函数前)静态成员使用前必须初始化 类型 类名::静态名 = ?; 对于public: 类名::静态名 = 10; 对象名.静态名 = 100; 对于private: 访问数据需要用,对象名.方法名 2、当修饰函数时: 不能访问非静态的类成员。 非静态成员可以访问静态成员 静态成员没有this指针。 this 指针:指向类的首地址。 只能在类的内部使用。 1、函数形参和类的成员同名时 2、返回本身这个类型。
关键字:const
c++新增内容: 1、const修饰类的成员变量: 修饰的变量不能更改,但必须初始化。(初始化列表) 初始化列表可以初始化const修饰的变量,也可以初始化非const修饰 也就是说,凡是const修饰,需要初始化时,必须使用初始化列表赋初值;非const修饰也可以用。 2、const修饰函数 格式: 返回值 函数名const()。 const修饰函数不能修改对象。 3、const修饰的对象 数据成员不能被改变
友元函数
1、打破了封装,不安全,避免使用。(打破封装也就意味着,可以访问受保护的内容) 2、需要频繁访问数据,使用友元会更快捷。关键字:friend 3、内部没有this指针 4、友元函数的声明可以放在类的私有部分,也可以是公有。因为没有区别,它不属于类。声明在类的任何位置都一样。 5、一个函数可以是多个类的友元函数,只需要在多个类声明。 6、友元和函数的调用和一般的函数的调用方式和原理一样。
友元成员函数
指:a类的成员函数是b类的友元,即成员函数在b类中声明了是b的友元。
友元类
1、友元关系不能被继承 2、友元关系是单向的,不具有交换性。如果b是a的友元,但a不一定是b的友元,要看是否在类中声明。 3、友元关系不具备传递性。主要还是看是否在类中声明。
运算符的重载
编译器无法识别自定义数据类型,所以需要运算符的重载。赋予运算符新功能,实现功能。 #include<iostream> using namespace std; * 运算符重载函数的基本语法: * 数据类型 operator<运算符>(<形参列表>) * { * //函数体 * } * 注意: * 1、数据类型 表示该函数返回值的数据类型; * 2、<运算符> 表示需要重载的运算符(C++ 中除了以下运算符不能被重载,其他都可以: * sizeof、.点号运算符、?问号表达式、::作用于访问符) * 3、operator<运算符> :构成函数名 * 4、运算符操作的对象是什么类型,那么运算符重载函数就应该设计在 哪个类。 * * */ class Demo{ public: // 成员变量都可以通过构造函数 参数初始化列表赋初值,const修饰的成员变量是 必须。 // 初始化列表中, 括号里面的是形参,外面的是成员变量 Demo(int x) : x(x){} int getValue(){ return x; }
class Demo{
public:
// 成员变量都可以通过构造函数 参数初始化列表赋初值,const修饰的成员变量是 必须。
// 初始化列表中, 括号里面的是形参,外面的是成员变量
Demo(int x) : x(x){}
int getValue(){ return x; }
// 1、成员函数运算符重载, 成员函数 默认拥有指向 对象的 this指针,在函数形参列表中隐藏(代表一个操作数)
// this指针 默认指向第一个操作数
// 因此 双目运算符 的成员运算符重载函数只能有 1个形参
// 2、成员函数运算符重载时,形参只能是 1个或 0个形参。
int operator+(Demo &obj);
Demo operator-(Demo &obj);
private:
int x;
};
int Demo::operator+(Demo &obj)
{
return this->x + obj.x;
}
Demo Demo::operator-(Demo &obj)
{
Demo c(this->x - obj.x);
return c;
}
int main(int argc, char *argv[])
{
Demo a(1), b(3);
// C++ 运算符默认的操作数的类型是 基本数据类型,无法计算两个类类型的对象 运算
// 因此必须在 类类型中重载 想要的运算符,才能实现运算(原理:运算符重载,实现新的功能:类对象运算)
cout << a + b << endl;
cout << a.operator+(b) << endl;
// cout << a - b << endl; //将a-b结果当成 int型来使用
Demo c(0);
c = a - b; //将 a-b结果当成 一个类对象来使用
cout << c.getValue() << endl;
//cout << a << b << endl;
return 0;
}
#include<iostream>
using namespace std;
class Demo{
public:
Demo(int x) : x(x){}
/*
* 友元运算符重载函数:
* friend 数据类型 operator(<形参列表>)
* {
* //函数体
* }
*
* 注意:
* 1、friend友元 打破类的封装,因此 运算符重载函数的形参个数 = 运算符的操作数个数。
* 2、当运算符的 第一操作数 为基本类型或者标准类型,就必须使用友元
* */
friend ostream & operator<<(ostream &out, Demo &obj);
friend int operator+(int x, Demo &obj){
return x + obj.x;
}
#if 0
friend int operator+(Demo &obj, int x){
return obj.x+x;
}
#else
int operator+(int x){
return this->x + x;
}
#endif
private:
int x;
};
ostream & operator<<(ostream &out, Demo &obj)
{
// 将 obj.x 这个数值 插入到标准数据流中
out << obj.x;
return out;
}
int main(int argc, char *argv[])
{
Demo a(13);
cout << a << endl; //运算符重载函数,只能是友元
cout << 1 + a << endl; //运算符重载函数,只能是友元
cout << a + 1 << endl; //运算符重载函数,既可以使用友元,又可以使用成员
/*
* cout 标准输出流对象,类型是 ostream &,相当于是给标准输出流 取的 别名为 cout
* */
return 0;
}
#include<iostream>
using namespace std;
class Demo{
public:
Demo(int x) : x(x){}
friend istream & operator>>(istream &in, Demo &obj){
in >> obj.x;
return in;
}
#if 0
int operator+(Demo &obj){
return x+obj.x;
}
#else
friend int operator+(Demo &obj1, Demo &obj2){
return obj1.x + obj2.x;
}
#endif
private:
int x;
};
int main(int argc, char *argv[])
{
Demo a(15), b(1);
cin >> a >> b; // 将标准输入流数据,先赋值给 a,然后将标准输入流剩下的数据,赋值给b
cout << a + b << endl; // cout << operator+(a, b) << endl;
return 0;
}
#include<iostream>
using namespace std;
class Demo{
public:
Demo(int x):x(x){}
#if 0
int operator++(){ // 前++
return ++this->x;
}
int operator++(int){ //后++,此处int没有任何作用,只是一个占位符,用来区分 前++
return x++;
}
#else // a++ ==> a = a + 1;
friend int operator++(Demo &obj){ // 前++
return ++obj.x;
}
friend int operator++(Demo &obj, int){ //后++,此处int没有任何作用,只是一个占位符,用来区分 前++
return obj.x++;
}
#endif
friend ostream &operator<<(ostream &out, Demo &obj){
out << obj.x;
return out;
}
private:
int x;
};
int main(int argc, char *argv[])
{
Demo a(12);
cout << ++a << endl; //cout << a.operator++() << endl;
cout << a++ << endl; //cout << a.operator++(int) << endl;
cout << a << endl;
return 0;
}
函数的数据类型,一般看返回值。如果函数返回的是类的成员,期待在函数外部能偶修改、使用该成员的内容,那么函数数据类型必须是 引用类型
模板
支持参数多态化的工具, 实质是设计一种通用类型的函数或类。在函数或类使用时让数据成员、成员函数、函数返回值可以是任意类型。 目的是,让程序员编写与类型无关的代码,是泛型编程的基础 ****模板的声明或定义只能在全局,命名空间或类范围内进行。即不能在局部范围、函数内进行,比如不能在main函数中声明或定义一个模板。
函数模板
针对函数参数类型不同的函数。 参数类型一样,但是功能及函数名一致的函数 语法结构: template <class T1, class T2 .....>T1 函数名(<形参列表>); emplate:模板说明关键字 类型模板形参列表: 用class 或 typename定义的变量,多个模板形参之间以空格隔开 class T1:定义类型模板形参T1,用来代替函数的相关数据类型 template<class T1> T1 add(T1 x,T1 y)//声明与实现 { 函数体 } template<typename T1> T1 aad(T1, T1)//声明 template<class T1>//实现 T1 add(T1 x, T1 y) { 函数体 }
* 函数模板语法结构:
* template < <类型模板形参列表> > 类型模板形参 函数名(<形参列表>);
* 注意:
* template:模板说明的关键字
* <类型模板形参列表>:用 class 或 typename 定义的变量,多个模板形参之间以 空格隔开
* 1、class T1: 定义类型模板形参 T1, 用来代替函数的相关数据类型, 简称 模板形参。
* eg:
* template<class T1> T1 add(T1 x, T1 y);
* */
#if 0
template<class T1> T1 add(T1 x, T1 y)//声明定义函数模板
{
return x+y;
}
#else
template<typename T1> T1 add(T1, T1); //函数模板声明
template<class T1> //函数模板定义
T1 add(T1 x, T1 y)
{
return x+y;
}
#endif
int main(int argc, char *argv[])
{
cout << add(1, 2) << endl; //函数模板的 隐式调用,自动匹配数据类型
cout << add(3.1, 2.3) << endl;
cout << add('a', '2') << endl;
cout << add< >(3, 4) << endl; //函数模板的 显示调用,自动匹配数据类型
cout << add< int >(5, 6) << endl; //显示调用,指定模板形参为 int类型,是将 int 传递给了 T1
cout << add< float >(3.12, 4.1) << endl; //显示调用函数模板,实参被隐式转换
cout << add< int >('b', '2') << endl;
return 0;
}
#include<iostream>
using namespace std
template<class T1, class T2>
T2 add(T1 x, T2 y)
{
return x+y;
}
int main(int argc, char *argv[])
{
cout << add(1, 2.3) << endl; //隐式调用
cout << add(1, 'c') << endl;
cout << add< >(2.3, 2) << endl;
cout << add<char, int >('a', 1) << endl; //显示调用
cout << add<char>('b', 2) << endl; //显示调用,T1传了 char, T2采用类型自动匹配
// cout << add<, float>('c', 1) << endl;
return 0;
}
#include<iostream>
using namespace std;
//带默认参数的模板形参
# if 0
template<class T1, class T2 = float>
T2 add(T1 x, T2 y)
{
return x+y;
}
#endif
template<class A, class B , class C = int> C add(A x, B y)
{
return x+y;
}
int main(int argc, char *argv[])
{
#if 0
cout << add(1, 2.3) << endl; //隐式调用
cout << add(1, 'c') << endl;
cout << add< >(2.3, 2) << endl;
cout << add<char, int >('a', 1) << endl; //显示调用
//显示调用,T1传了 char, T2采用类型自动匹配, 因此模板形参的默认参数 无意义。
cout << add<char>('b', 2) << endl;
#endif
cout << add<int, char, int>(3, 'a') << endl;
cout << add<int, char>(3, 'a') << endl;
return 0;
}
类模板
针对数据成员,成员函数数据类型、参数类型不同的类。 成员属性的类型和成员函数类不一样,但成员属性及函数一样的类。 类模板的模板形参可以有默认值。
#include<iostream>
#include <assert.h>
using namespace std;
* 设计一个通用函数,函数功能,就是为 指针开辟堆区空间,空间大小由使用函数时决定。
*
* int * newPoint(int *p, int max)
* {
* p = new int[max];
* return p;
* }
#define max 6
// T :类型模板形参,用来替换 数据类型
// m :非类型模板形参,用来替换函数模板中的常量值 ( 整型、指针、引用)
template<class T, int m = max> T * newPoint()
{
T *p = new T[m];
assert(NULL != p);
return p;
}
int main(int argc, char *argv[])
{
char *p = NULL;
//p = newPoint<char, max>(); //调用的是第二个函数模板,元素个数有模板形参列表中 决定
p = newPoint<char>();
for(int i = 0; i < max; i++){
cin >> p[i];
}
for(int i = 0; i < max; i++){
cout << p[i] << " ";
}
cout << endl;
return 0;
}
#include<iostream>
using namespace std;
1、如果一个类成为了类模板,那么这个类中所有成员函数 将自动变成 函数模板。
template<class T>
class Demo{ //声明定义一个类模板
public:
#if 0
Demo(T x) : x(x){}
void setValue(T x){
this->x = x;
}
T getValue(){
return x;
}
#else
Demo(T );
void setValue(T);
T getValue();
#endif
private:
T x;
};
template<class B> //类模板成员函数在类外部定义的 语法结构
Demo<B>::Demo(B x): x(x){}
template<class A>
void Demo<A>::setValue(A x)
{
this->x = x;
}
template<class C>
C Demo<C>::getValue()
{
return x;
}
int main(int argc, char *argv[])
{
Demo<int> obj(23); //类模板定义对象时,必须显示说明使用的 类模板<>,并且必须传递数据类型
obj.setValue(45);
cout << obj.getValue() << endl;
return 0;
}
#include<iostream>
using namespace std;
template<class T, int max = 12>
class Array{
public:
Array(T len){
for(int i = 0 ; i < max; i++)
arr[i] = len;
}
T getValue(int id);
private:
T arr[max];
};
template<class A, int max>
A Array<A, max>::getValue(int id)
{
return arr[id];
}
int main(int argc, char *argv[])
{
Array<int, 6> obj(12);
cout << obj.getValue(0) << endl;
return 0;
}
类模板与友元函数。
#include<iostream>
using namespace std;
template<class T> class Demo{
public:
Demo(T x) : x(x){}
#if 0
friend T operator+(Demo<T> &a, Demo<T> &b){//声明、定义一起写
return a.x + b.x;
}
#elif 0
friend int operator+(Demo &a, Demo &b){//这里就不是模板了
return a.x + b.x;
}
#elif 1
template<class A>//分开写时,声明与定义都必须用template说明
friend A operator+(Demo<A> &, Demo<A> &);
#endif
private:
T x;
};
#if 0
#elif 1
template<class T>//分开写时,声明与定义都必须用template说明
T operator+(Demo<T> &obj1, Demo<T> &obj2)
{
return obj1.x + obj2.x;
}
#endif
int main(int argc, char *argv[])
{
Demo<int> obj1(2), obj2(3);
cout << obj1 + obj2 << endl;
return 0;
}
继承
继承:一个新类从已有的类哪里获得其已有的特性。 类的派生:从已有的类的基础上产生一个新的子类。 派生类继承了基类的所有数据成员,和大部分成员函数(构造与析构函数没有继承)。 派生类是基类的具体化,而基类是派生类的抽象。 继承格式: calss 派生类名 :派生说明符 基类名{ 派生类主体 };
继承规则
1、再c++中一共有三种派生方式说明符号:public、protected、private。分别对应 公有继承、保护继承、私有继承。当省略说明符时,默认为私有继承。 2、派生后,派生类中会继承基类的所有数据成员,和大部分成员函数(构造与析构函数没有继承)。不同的继承方式,访问权限略有不同: 第一:公有继承时,原基类权限在派生类的权限不变。 第二:保护继承时:原public权限,在派生类中变为protected 第三:私有继承时:原public、protected权限,在派生类中都变为private权限 访问权限总结: 2.1、无论什么权限,在派生类中,都可以直接访问 原public 和 pritected权限数据,不能直接访问私有权限。 2.2、通过派生对象访问基类数据时,除了公有继承时,能直接访问原public权限数据。其他两种继承方式,原public权限在派生类中都不在是public权限,无法直接访问。 2.3、当派生类中自己的成员与基类成员同名时,会自动隐藏基类成员。 在基类成员被隐藏的情况下,在派生类或派生类对应的对象中直接调用同名成员时,默认调用派生类自己的成员。如需要访问继承过来的基类成员需要加域名解析符进行说明: 基类名::成员 宏函数:pri();代码中调用,打印当前函数名与行号,方便调试代码。 #define pri() cout<< __func__ << " line: " << __LINE__ << emdl;
继承时的构造函数
前提:在一个类中如果声明有 有参构造函数,那么这个类的默认构造函数就不会生成。也就是说在创建这个类的对象时,需要根据类中构造函数的格式传参. 当定义派生类对象时,派生类会自动调用(在派生类的构造函数的初始化列表中隐式调用) 基类的默认构造。
情况一:无参构造函数
class Base{
public:
Base(){ pri(); }
private:
int x;
};
class Subclass : public Base{
public:
Subclass() : Base(){//隐式调用基类构造函数
pri();
}
};
int main(int argc, char *argv[])
{
Subclass son;
}
情况二:有参构造函数
class Base{
public:
Base(int x):x(x){ pri(); }
private:
int x;
};
class Subclass : public Base{
public:
Subclass(int x) : x(x), Base(12) { pri();}
private:
int x;
};
int main(int argc, char *argv[])
{
Subclass son(25);
return 0;
}
所以,定义派生类对象时,先调用基类构造函数(存在继承于多个基类时,先调用有virtual修饰的基类,再从左到右依次调用其构造函数),再定义派生类中的成员,最后才是执行派生类本身构造函数。 派生类中的组成,一部分为派生类自己的成员,另一部分为在基类继承的成员。当两部分名字一样时,存在名字二义性。访问继承的成员需要使用域解析符来访问继承的成员。
构造中的路径二义性
多重继承时,当一个类派生于另一个类的多个派生类,那么会出现构造函数的路径二义性的问题。 解决方法: 1、域名解析符 2、虚继承(virtual)
#define pri() cout<<__func__<<" line: "<<__LINE__<<endl;
class animal{
public:
animal(){ pri(); }
~animal(){ pri(); }
int x = 100;
};
class man : virtual public animal{
public:
man(){ pri(); }
~man(){ pri(); }
};
class wolf : virtual public animal{
public:
wolf(){ pri(); }
~wolf(){ pri(); }
};
// 多重继承时,虚拟继承的基类 优先被构造
class wolfman : public wolf, virtual public man{
public:
wolfman(){ pri(); }
~wolfman(){ pri(); }
};
int main(int argc, char *argv[])
{
wolfman obj;
// 错误:有歧义,路径二义性: 因为animal基类被构造 2次,内存基类部分有两份不同的空间,也就是 x存在于两个地址空间
// cout << obj.x << endl;
// 路径二义性解决方式 1:作用域访问符
//cout << obj.wolf::x << endl;
//cout << obj.man::x << endl;
继承中的析构顺寻
如果是单继承:先析构派生类的析构函数,再析构基类 如果是多继承:先析构派生类析构函数,再从左到右析构基类
继承时的类型转换
基类(大范围),派生类(小范围)。当存在类型不符时,可以由派生类转为基类(隐式转换)即向上隐式转换。但不允许隐式转换:基类转派生类。 从内存角度看:派生类中不仅有自身的成员,还有在基类继承的成员,当发生向上类型转换时,会自动丢弃派生类自己的成员只保留继承的成员。
不写权限限定词,默认私有继承。 1、任何继承,在派生类内部,都能访问pbulic、protecteed。不能访问private 2、派生类对象访问时 共有继承:基类所有成员权限不变。 保护继承:公有变保护。 私有继承:公有和保护变私有。 不允许向下(大范围转小范围)隐式转换,因为对象空间中没有派生类部分数据。 公有继承,可以向上隐式转换(小范围转大范围),将 派生类对象隐式转换成 基类类型,只保留了派生类对象中基类部分的内容,然后进行复制操作。 构造析构顺序:派生类只继承一个基类 单继承:构造和析构执行顺序相反,先基类构造在派生类构造,先派生类析构在基类析构。 is——a关系:就是公有单一继承。派生类对象也是一个基类对象。 has_a关系(聚合/组合):一个类中有另外一个类的对象。 多重继承: 使用作用域访问符,解决多重继承中名字而重性问题。 构造顺序,先左起第一个基类构造,再第二个基类构造.....最后派生类构造 析构顺序,先派生类析构,再右起第一个基类析构,再右起第二个..... 多重继承中,虚继承优先级更高。 c++11后,可以直接在类体中定义初始化 成员变量。
多态
一个接口,多种方法。程序再运行时才决定调用哪个方法,是面向对象编程的核心概念。希望基类与子类有相同的方法,但行为有所不同。 多态性:将接口与实现进行分离,实现共同的方法,但因个体差异,采用不同策略。
面向对象编程特点
封装:实现细节的隐藏,使代码模块化。 继承:扩展已存在的代码,代码重用。 多态:实现接口重用,无论传递过来的是那个类的对象,都能通过调用同一个接口,调用到各自适应的实现方法。
多态的实现
使用 virtual修饰的成员函数(虚函数)。 虚函数的条件: 1、非类的成员函数不能设置为 虚函数。 2、类的静态成员不能定义为 虚函数。 3、构造函数不能定义为 虚函数,构造函数不能被对象调。析构函数可以定义为虚函数。 4、成员函数声明时需要使用 virtual 关键字,定义时不需要 5、基类的成员函数设置为虚函数,派生类中同名函数(函数名、形参类型、个数、返回值全一样)自动成为虚函数。
class Base{
public:
virtual void priMsg(){
cout << __func__ << " line: " << __LINE__ << endl;
}
int priMsg(int x){}
};
class Subclass : public Base{
private:
void priMsg(){
cout << __func__ << " line: " << __LINE__ << endl;
}
int priMsg(int x){}
};
void test(Base *p)
{
p->priMsg();
}
int main(int argc, char *argv[])
{
Base obj1;
Subclass obj2;
test(&obj1);//调用的是基类中的primsg
test(&obj2);//调用的是派生类当中的prinsg
return 0;
}
由于基类中为虚函数,所以派生类中也会自动转换为虚函数。在向上隐式转换时派生类中的虚函数会重写基类中的虚函数。数以调用的是派生类中的函数。
成员函数的覆盖也称重写
指派生类重新定义基类的虚函数,特征如下: 1、不同的作用域(分别位于派生来与基类) 2、函数名相同 3、基类函数必须有virtual关键字,不能有static 4、返回值相同 5、重写函数的权限访问限定符可以不同 重写是指派生来重定义基类虚函数:所以函数需要同名(函数名、参数类型与个数以及返回值都是一样的)、虚函数也就意味者基函数需要virtual修饰、分别在基类与派生来所以作用域不同。另一个注意就是,重写函数的限定符意思是派生类中的限定符。
函数的重载
指函数名相同,但参数不同(数量、类型、次序)特征: 1、相同范围(同一个作用域中) 2、函数名相同 3、参数不同 4、virtual参数可有可无 5、返回值可以不同
成员隐藏(重定义)
成员隐藏发生,派生类与基类成员函数名相同时。派生类的成员函数会隐藏基类成员函数。需要访问到基类成员函数需要加域名解析符。 1、函数名相同 2、返回值可以不同 3、参数不同时,此时,不论有无virtual关键字,基类函数都会被隐藏。此时和重载的区别在于,作用域不同。而重载函数需要作用域相同。 4、参数相同时,但是基类函数没有virtual关键字,此时,基类函数被隐藏
函数重写(覆盖)、重载、重定义(隐藏)
重载必须在同一作用域,函数名必须相同,但是返回值和参数可以不同。 重写与重定义发生在存在派生关系的类之间。即作用域不同。 重写条件比较苛刻,要求基类要有virtual修饰,同时参数、返回值必须一样。 重定义强调的是函数名相同,派生类调用时无法区分是基类的还是派生类的。名字的二义性
联编
就是将模块或则函数合并在一起生成可执行代码的处理过程。按照联编进行的阶段不同,可分为两种不同的联编:静态联编、动态联编。也就是C语言中的静态连接与动态连接 一旦涉及多态与虚函数就必须使用动态联编,因为多态的特点就是,同一接口,多种方法。只有在运行的时候才能确定(也就是晚绑定)到底运行那个成员函数。
晚绑定
就是程序在运行是才绑定所需要的库。 晚绑定的实现: 在每个对象的开头都有一个地址,这个地址是虚函数表的地址。虚函数地址表中存放的是虚函数的地址。
抽象类
含有存虚函数的类就是抽象类 存虚函数格式: virtual 返回值类型 函数名(形参列表) = 0; 抽象类特点: 1、抽象类没有完整的信息,只能是派生类的基类 2、抽象类不能有实例,不能有静态成员 3、派生类应该实现抽象类的所有方法
虚析构函数
为什么要使用虚析构函数,而且一般都推荐使用虚析构函数。 原因在于,当产生了一个派生类对象后,并发生类型隐式向上转换时,派生部分空间就找不到了。这时候去释放资源的化,派生类的资源没有成功释放。会造成内存泄漏。 当使用虚析构函数时,发生向上隐式转换,基类虚函数会被重写为派生类虚函数。这样在释放时,就不会存在找不到派生类资源的问题。这也是多态的一个运用
限制构造函数
一个类的构造函数的访问权限不断public,那么这个构造函数就是限制构造函数
异常
异常是一种容错机制,是一种错误处理技术。 基本思想:让一个函数在发现了自己无法处理的错误时抛出一个异常,然后让它的调用者能够处理这个问题。 目的:提高程序的稳定性和健壮性 exception: 所有标准异常类的父类 用户调用函数,处理异常的格式: try{ 调用接口函数 }catch(捕获异常) { 处理异常 }
标准异常处理规则
标准异常需要包含头文件: stdexcept http://www.cplusplus.com/reference/stdexcept/ 所有标准异常类的父类 class exception{ public: execption() noexcept; exception(const exception&)noexcept; exception& operator= (const exception&) noexcept; virtual ~exception (); virtual const char* what()const noexcept; }; 一些常见的派生类: bad_alloc:请求分配内存失败 logic_error:逻辑错误类 domain_error:域错误类 invalid_argument:无效参数类 length_error:长度错误类 out_of_range:超出范围异常类 runtime_error:运行时错误异常类 range_error:计算结果超出了有意义的值域范围 overflow_error:算术运算上溢出错误异常类 underflow_error:算术计算下溢错误类 低版本的c++,会在函数声明与定义时说明,函数可能抛出的错误类型。c++后就默认所有函数都有可能抛出错误。在自定义函数中,如果想想设计为不会抛出异常的函数,就需要在函数的声明与定义后加 noexception。
//低版本,显示说明函数抛出什么类型的异常
float func(int x, int y) throw( invalid_argument );
float func(int x, int y) throw( invalid_argument)
#elif 0
//C++11标准以后,不需要显示说明该函数抛出什么类型的异常,因为 默认所有函数都会抛异常
float func(int x, int y);
float func(int x, int y)
#else
//如果函数声明定义为 noexcept,表示该函数不会抛出异常。如果函数中强行throw(),会出现段错误
float func(int x, int y) noexcept;
float func(int x, int y) noexcept
#endif
{
if( y == 0 ){ //检查错误
// return -1;
#if 0
//定义无效参数类型的 对象,用想传递的错误信息初始化
invalid_argument tmp("error, y = 0, needs y != 0");
//抛出类对象,就是在抛出异常,立即结束该函数向下执行
throw tmp;
#else
// 抛出匿名对象
throw invalid_argument("error, y=0, but need y != 0");
#endif
cout << "------------" << endl;
}
return x/y;
}
int main(int argc, char *argv[])
{
int a, b;
cin >> a >> b;
try{
cout << func(a, b) << endl;
} catch ( exception &err){ //捕获异常, 如果不知道函数会抛出什么类型的异常,那么捕获时 使用 exception类型
//打印异常保存的错误信息
cout << err.what() << endl;
//相应的异常处理
}
return 0;
}
自定义异常处理
class MyException{
public:
MyException(const char *err) noexcept : errmsg(err){ }
const char * priError() const noexcept {
return errmsg;
}
private:
const char *errmsg;
};
float func(int x, int y)
{
if(y == 0)
//抛出完全自定义异常类
throw MyException("error, needs y!=0");
return x/y;
}
int main(int argc, char *argv[])
{
int a, b;
cin >> a >> b;
try{
cout << func(a, b) << endl;
} catch ( MyException &err){
cout << err.priError() << endl;
}
return 0;
}
转换函数
c++中提供类型转换函数,将一个类的对象转换成另一类型的数据。 转换函数的实质就是运算符重载,只是重载的运算符不是内置的运算符而是类名这个特殊的自定义类型。 语法形式: operator 类型名(){ 实现转换的语句 } 基本规则: 转换函数只能是成员函数,无返回值,空参数。 不能定义void的转换,也不允许转换成数组或者函数类型。 转换常定义为const形式,原因是它并不改变数据成员的值。 explicit关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换。给单参的构造函数使用该关键字,会阻止产生的隐式转换:由成员变量转换为类类型。 标准转换函数: 编译时运转换:reinterpret_cast const_cast static_cast 运行时转换:dynamic_cast reinterpret_cast: reinterpret_cast<new type>(expression) 将一个类型的指针转换为另一个类型的指针,它允许从一个指针转换为整数类型。 const_cast<new type>(expression) const指针与普通指针间的相互转换,不能将非常量指针变量转换为普通变量 static_cast<new type>(expression) 主要用于基本类型间的相互转换,和具有继承关系的类型转换 dynamic_cast<new type>(expression) 只有类中含有与函数才能用,仅在继承类对象间转换。 自定义转换函数 #include<iostream> using namespace std; //转换函数,将当前类型转换成 int类型(将类类型,转换成其他的任何类型(void除外)) //语法: // operator 类型(){ // // } // 注意: // 1、转换函数没有数据类型,但是有 return返回值 // 2、转换函数必须是类的成员函数,而且空参数 // 3、不能定义到 void 、数组、函数类型的转换 // 4、转换函数常为 const修饰 // 实质: // 运算符重载,只是重载的运算符只能是 数据类型
class Subclass;
class Base{
public:
Base(int x) : x(x){}
int getValue(){ return x; }
operator int(){ return x; }
operator Subclass();
private:
int x;
};
class Subclass : public Base{
public:
Subclass(int x, int y) : Base(y), x(x){}
int getValue(){ return x; }
private:
int x;
};
Base::operator Subclass(){
return Subclass(x, 0);
}
int main(int argc, char *argv[])
{
int a = 3;
Base obj(5);
obj = a; //移动构造(重载了 =运算符)
cout << obj.getValue() << endl;
obj = 97;
a = obj; // 调用类 Base 中的转换函数, 转换为 int类型
cout << a << endl;
char ch = obj;
cout << ch << endl;
Base obj1(3);
Subclass obj2(5, 1);
obj1 = obj2;
cout << obj1.getValue() << endl;
obj1 = 23;
obj2 = obj1; //调用Base类中的转换函数,转换为 Subclass类型
cout << obj2.getValue() << endl;
return 0;
}
智能指针
智能指针是个特殊的类模板,重载了“->” 、“*”运算符,实现了c++的自动内存回收机制。 智能指针通用实现技术是使用引用计数。智能指针类将一个计数器与类指向对象相关联,引用计数跟踪该类有多少对象指针指向同一对象。 shared_ptr unique_ptr weak_ptr
#include<iostream>
using namespace std;
#include <memory>
class Base{
public:
Base(int x): x(x){ cout << "line: " << __func__ << endl; }
~Base(){ cout << "line: " << __func__ << endl; }
int getValue(){ return x; }
private:
int x;
};
void test()
{
//定义资源共享的智能指针 p(实质是一个类模板的对象),
//当该函数结束时,该对象的生命周期将会结束,系统会自动调用析构函数回收堆区资源
#if 0
shared_ptr<Base> p(new Base(12));
#else
// make_shared<>():函数模板,目的将堆区开辟的空间,进行共享处理,安全性比 new更高
shared_ptr<Base> p = make_shared<Base>( Base(13) );
#endif
//智能指针 p,重载 ->和*号运算符,因此访问 成员必须用 -> 运算符
cout << p->getValue() << endl;
//定义另一个shared_ptr指针,指向同一个对区
shared_ptr<Base> q = p;
}
int main(int argc, char *argv[])
{
test();
return 0;
}