C++学习笔记

一 .C与C++区别

C++的介绍:

C++是一种计算机高级程序设计语言,由C语言扩展升级而产生,最早于1979年由本贾尼·斯特劳斯特卢普在AT&T贝尔工作室研发。
C++既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行以继承和多态为特点的面向对象的程序设计。C++擅长面向对象程序设计的同时,还可以进行基于过程的程序设计。
C++拥有计算机运行的实用性特征,同时还致力于提高大规模程序的编程质量与程序设计语言的问题描述能力。 (来源百度百科)

从上述介绍中,我们可以提炼出:
- C++是对C语言的扩展升级 ----------->> C的多数特性和语法规则在C++中依然适用。
- C++是一个面向对象的程序设计语言----------->> 相对于面向方法的C语言,C++语言提出了面向对象的设计思想与 类 Class的概念

除此以外,为了方便封装各种抽象的数据类型,C++提供了功能强大的STL库,该库中有封装好的各种常用数据结构;同时,C++也提供了template类模板的方法,通过template,我们可以创造一个类模板或函数模板,再通过提供不同类型的数据,使其产生处理对应数据类型的模板类与模板函数,这样极大提高了C++代码的复用性与可移植性。

因此,可以说 C++ = C + Class + STL + Template

1.1 const与常量

在原来的认识中,被const修饰的量其值不可被更改,我们称这样不可被修改的量为常量。但在C语言中,有些变量即使被const修饰,但在某些情况下其值仍可被修改,如下例:

	const int b = 10;
	int* p = (int)&b;	//通过强制类型转换,p可以获取b的地址
	*p = 20;       		//通过修改*p来修改b的值
	printf("b = %d  ,*p = %d \n"); // 结果为 b = 20, *p = 20;

因此,c语言中的const修饰的值仍可被改变,我们称这样的值为 常变量 而非常量。

而在C++ 中,由const修饰的量为常量,其值在任何情况下均不可被修改;此外,常量还具有如下特性:

  • 编译期常量的值被直接写入到常量的声明点;因此常量必须初始化。(在c++编译规则中)

  • 常量的初始化必须使用常量, 如果使用变量给const修饰的量初始化,则该量(const )会退化成常变量;如:

int b = 1const int c = b;//此时c退化为常变量,其值可被修改

const与指针

	int a =10;
	const int * p1 = &a; //const修饰了int(即p1指针指向的是一个const int类型变量的地址),因此不可通过p1改变a的值
	int * const p2 = &a; //const修饰指针p2,因此指针p2的值(即指向的地址)不可改变
	const int * const p3 = &a; //p3兼具以上两种特性
	int a =10;
	int * p1 = &a;

// 以下3语句均可编译通过
	const int * p2 = p1; 
	int * const p3 = p1; 
	const int * const p4 = p1; 

被const修饰的指针与其他被const修饰的指针互相赋值时——

//例1

int a =10;
//const修饰了int,因此通过P对a只可读而不可写 (因为a为const int类型,为常量)
const int * p = &a;

int * p0 =p; // 编译不通过,p0可读可写,能力发生扩展
const int * p1 = p; //通过
int * const p2 = p; //不通过,p2仍可读可写
const int * const p3 = p;//通过
//例2

int a =10;
//通过P对a可读可写;只是p的指向不可改变
int * const p = &a;

int * p0 =p; // 编译通过
const int * p1 = p; //通过,p1只可读,能力收缩是允许的
int * const p2 = p; //通过
const int * const p3 = p;//通过

1.2 引用(别名)

从语法层面讲 — 引用是对同一空间所起的不同名称
在系统底层来看,引用仍然是由指针实现的(*const)

对数组的引用:

int ar[10] ={ 12,23,34,45,56,67,78,89,99,100 }
int& a = ar;             //错误
int (&b)[10] = ar;       //正确(同时给出 变量类型 与 个数)

当函数返回值为引用时::

int fun()
{
   int a = 10 ;
   return a;
}

int& funref()  //编译为int * const funref()
(
    int a = 20 ;
    return a;   //编译为return &a;注意,此处返回值为a的地址
}

int main()
{
int x = fun () ;
int y = funref();     //避免如此编程,函数返回值为&a,此处编译为int y = *funref();对局部变量进行引用返回可能访问随机值(访问将亡值地址可能读取错误数据)

int& z = funref() ;   //同上
}

当变量生存期不受函数影响(如static,全局变量,堆空间申请 )时,可以以引用方式返回(即避免访问临时量的空间)

当函数返回值为对象的引用时;系统会在该函数区通过拷贝构造函数构建一个临时对象,因此返回数据可能受到侵扰;

若返回值为对象class,无名对象会直接构建在主函数空间内

class&  a()
{
  return class(); //此处返回无名对象
}

*引用与const

例1::

int a =10;

int * p = &a;

int *&pref1 = p; //通过,按照从右向左结合的规则解释,pref1首先为一个引用(&),其后pref1为一个指针的引用(*),且为整形(int)指针的引用;因此 pref1为p的别名

int &*pref2 = p; //不通过,此处解释为pref2为一个指针(*),其指向为一个引用(&);语法规则上错误,不认为引用具有地址

例2::


int a =10;
int *s =&a;

int *&p1 =s; // 编译通过

//不同的编译器可能不允许下列部分指针的声明
const int *&p2 = s; //通过,但不可修改*p2的值(*s的值仍可修改),修改p2的指向时,s的指向也会发生变化
int * const &p3 = s; //通过,但不可修改p3的指向,仅当s的指向改变时,p3指向改变
const int * const &p4 = s;//通过

例3::


int a =10;
const int *s =&a;

int *&p1 =s; // 错误,能力扩展(s可读不可写,p1可读可写)
const int *&p2 = s; //通过
int * const &p3 = s; //错误
const int * const &p4 = s;//通过

例4::


int a =10;
 int * const s =&a;

int *&p1 =s; //错误,p1修改会导致s修改
const int *&p2 = s; //错误
int * const &p3 = s; //正确
const int * const &p4 = s;//正确

总结:普通引用可以用常引用与普通引用,产量只能用常引用

引用的底层实现是一个指针
在编译期,引用会自动替换为底层指针的解引用

//a,*p,x,y是同一空间
void fun(int &a) //void fun(int * const a)
{
    int* p = &a;
    a = 100;  //底层以指针形式处理 *a = 100
    *p = 200;
}

int main()
{
   int x = 10;
   int &y = x; // int * const y = &x;
    fun(x);
    fun(y);
   return 0;
}

引用为什么要初始化?

  • 在编译期,引用需要替换为解引用

引用初始化后为什么无法被改变?

  • 在编译期被替换为解引用;其实现为 *const

当引用一个不可以取地址的量时,使用常引用;

临时量都有常量属性

1.3 默认值参数

在函数声明或定义时,给定参数默认值。

如果实参传递时不为形参传值,会按默认值赋值。


 int fun(int a,int b = 9,int c = 10)// 默认值参数
 {
	 count << a << endl;
	 count << b << endl;
	 count << c << endl;
 }
 
 int main()
 {	 
	 fun(1,2);//正确,此时fun中a=1,b=2,c=10
	 fun(1,,3);//错误,需依次赋值
	 fun(1,(2,3),4);//正确,此时fun中a=1,b=3,c=4
	 return 0;
 }

注意:
a.参数默认值在编译期生成指令时,直接生成入参指令。
因此,默认值只可传递常量。(变量在编译期无法获取变量值。)
因此,默认值只在本文件生效。(编译只针对单文件)

b.默认值只可从右向左依次设置默认值。不能跳过。
可通过以下方式连续设置默认值。

 int fun(int a,int b,int c = 10)// 默认值参数
 {
	 count << a << endl;
	 count << b << endl;
	 count << c << endl;
 }
 int fun(int a,int b = 20,int c)// 默认值参数

c.默认值参数在同一作用域中不可多次赋值。

1.4 内联函数

1)内联函数的使用是为了解决 频繁调用小函数而大量消耗栈空间 的问题

因此可将其视为“空间换时间”的一种办法

消耗栈空间:这里主要指现场保护与恢复,开辟栈帧,及栈帧回退的时间

  • 正常函数在调用时—
    1 .传参
    2 . call fun //调函数
    3 .开辟栈帧
    4 .返回值返回
    5 .栈帧回退
    6.参数清除

2)内敛函数调用

int fun(int a,int b)
{
	return c = b + a;	
}
 
int main()
{
	int a = 1 ,b = 2;
	
	int c = fun(a,b);//普通函数
	int c = a + b;   //内敛函数,编译期在调用点展开
	
	return 0;
}

3)inline不总是展开
在debug版本(调试版本)下与常规函数无差异,不展开;
在release版本(发行版本)下使用,该函数会在调用点展开(编译时期);

注意
a. 递归函数无法被展开
(编译时无法获取变量值,因而无法实现终止条件);

b. inline只是对系统建议将该函数处理为内联函数,在函数体过大(如行数大于5)或过于复杂(存在循环结构,if语句等)时,编译器会将其视为普通函数处理;少部分编译器甚至会报错

c. inline在debug版本生成的是local符号,只在本地可见;如果处理为内联之后在release版本不生成符号,直接在调用点展开。

函数展开调试类型安全校验栈帧的开辟可见性符号
宏函数预编译时期在调用点展开无法调试单文件可见不生成
static函数不展开可调试单文件可见生成local符号
内联函数debug版本不展开,release版本(在编译阶段时)于调用点展开可调试debug版本有栈帧开辟;release版本没有栈帧开辟单文件可见debug版本生产local符号,release版本不生成符号
普通函数不展开可调试多文件可见生成global符号

符号
所有的数据都会生成符号;指令中只有函数名会生成符号
分为——1 .全局符号 global 符号 2.局部符号 local 符号
只有本文件可见

因为内敛函数会进行类型检查与安全检查,可将其视为更安全的宏

1.5 函数重载

在了解重载机制前,我们应当先了解C++是以函数原型来区分不同函数

函数的原型 包括函数返回类型,函数名,形参列表(参数个数,类型,顺序)

函数重载:函数名相同,参数列表不同。

int Max(int a,int b);
double Max(double a,double b); //实现了Max函数的重载

注意:函数重载仅以参数列表作为重载判断条件,返回类型不同不构成重载!
不以返回类型区分的原因:

  • 调用时产生二义性
  • (对上一点的补充)在调用时各函数均符合其调用规则,此时便无法调用
int Max(int a,int b);
double Max(int a,int b); //仅是返回类型不同,非重载函数

int main()
{
	//···
	int a = 1;
	int b = 2;
	Max(a,b); //错误,调用具有二义性,系统无从判断应当调用那个函数
	//···
}

另外值得注意的是,即便可以通过参数个数不同实现函数重载,但在某些设置默认值的情况下依然会因为产生二义性而错误,如下面的情况:

void fun(int a,int b)

void fun(int a,int b,int c = 0)//设置默认值参数

int main()
{
    fun(12,23);        //错误,产生二义性
    fun(12,23,34);    //正确
    return 0;
}

函数重载是在编译时期决定的调用哪一个函数——这是C++静多态特性的一种表现

C++能进行函数重载的原因——编译时使用了重命名规则—即 名字粉碎 技术

1.6 C与C++函数的互相调用

C++调用C
使用extern “C”

//使用C语言的方法编译下面代码
extern “C”  int  fun(int a,int b)
{
	//···;
}

C调用C++
在C工程中添加实现的C++文件,写C++函数作为中间层,用中间层调用需要的C++函数,需要自实现的C++函数产生C语言符号,之后使用C语言调用。

1.7 函数摸板

//函数模板
//此处T仅作标识符用,可替换为任意字符
//也可写作 template<class T>
template<typename T>
void swap(T& a, T& b)
{
   T tmp = a;
   a = b;
   b = tmp; 
}


int main()
{
   int a = 10, b = 20;
   double da = 12.33, db = 23.23;
   char ch1 = 'a',ch2 = 'b';

   swap(a,b);//判断a,b为int类型,swap函数模板生成与之对应的模板函数
   swap(da,bb);
   swap(ch1,ch2);
 //  自定类型(struct)也可以调用函数摸板
}

函数模板类型的推断发生在编译时期

注意* 函数摸板的实现不能理解为简单的替换
如在上例中

template<typename T>
void swap(T& a, T& b)
{···}

int main()
{
  ···
  swap(a,b);
}

类型转换的实现并非为宏的替换-swap(int& a,int& b)
而是使用类型重命名规则(根据实参推演出形参-函数模板生成的函数称为模板函数)
typedef int Type
void swap<int> (Type& a,Type& b)
函数模板生成模板函数,其关系如同class与其实例的关系,后者由前者生成
例2:

void fun(T a) //完全泛化
{
  T x,y;
cout<< "T type : "<<typeid(T).name() << endl;

cout<< "a type :" << typeid(a).name() << endl ; 
}

void fun1(T* a) //部分泛化
{
  T x,y;
cout<< "T type : "<<typeid(T).name() << endl;

cout<< "a type :" << typeid(a).name() << endl ; 
}

int main()
{
  int x = 10;
  const int y = 10 ;
  fun1(x); //无法编译通过,fun1作为部分泛化仅接受指针
  fun1(y);//同上
  fun1(&x); //T推演为int
  fun1(&y); //T推演为const int
  fun(x); //T推演为int类型
  fun(&x); //T推演为int*
  fun(y); //T推演为int
  fun(&y) ;//T推演为const int *
  
  int* xp = &x;
  const int* yp = &y;
  fun(xp); //T 推演为int*
  fun(yp); //T 推演为const int*
}

数组引用与函数模板

在这里插入图片描述

  • 在处理时,函数模板创建多个函数
  • 非类型变量在编译时即会被替换,所以其并非一个变量,在此处可看作是宏替换

1.8 new与malloc

int main()
{
    ip = new int // 在堆区申请一个整形空间
    *ip = 100delete ip; //释放空间
    ip = NULL;
     
    ip = new int[n];//申请n个整形空间
    //申请对象数组时需要默认值参数或缺省构造函数
    delete[]ip; //释放连续申请的空间
    ip = NULL}

区别:
malloc申请失败需要自行判断;if(ip == NULL)
new申请失败会抛出异常 throw bad_allloc
ip = new(nothrow) int[n]表明不需要抛出异常,申请失败时赋值为空

若数组空间内对象为自设类型,delete会连续调用该对象的析构函数;对于系统内设类型,free与delete在使用上无差别

1.9命名空间

using namespace std;
命名空间用来解决全局变量名污染问题(名字重复)
如在工程A与工程B中均定义了函数fun;
在主函数调用时会产生二义性,此时使用命名空间:

int fun(){···};  //假设该函数定义在工程A中
int fun(){···};   //假设该函数定义在工程B中

int main()
{
    fun();//错误,二义性
}

做如下修改:

namespace A
{
   fun(){···}
}
namespace B
{
   fun(){···}
}

int main()
{
    A::fun(); //此处::为作用域解析符
    A::fun(); 
}

*C++中的右值引用

C++中的右值引用

二.面向对象

2.1 面向对象

对象即是现实世界中某个具体的物理实体在计算机逻辑中的映射和体现

class :是一组相关的属性(变量)与行为(方法)的集合,是一个抽象概念设计的产物。
c++中,类是一种数据类型

  • 成员变量是对象的属性,属性的值确定对象的状态。
  • 成员函数是对象的方法,确定对象的行为。

面向对象三大特性:封装 继承 多态 (抽象)

类的设计:

//class为数据类型说明符
class student
{
  public:
     bool sex;
     void set_age(int age);
  privateint Age;
     char name[10];
};

//对象方法的类外声明(此时函数明与参数列表需相同)
void student::set_age(int age)
{
   Age = age;
}

int main()
{
   student c1;//创建对象
   c1.sex = 0;//合法
   c1.Age = 10;//错误,不可访问private成员
}

如成员可见性不进行设置,默认为private
private与protected体现了类具有封装性

一般将变量设置为private,方法设置为public;
这样,就仅可靠对外的接口来改变变量的值

  • 注意,空的类大小为1而非0(用来让系统标识类在系统空间中的位置)

在创建多个对象时,不同对象的属性存放于不同区域;全局对象存放于数据区,在函数内部定义则为栈区,用new构建则为堆区

而方法只在代码区创建一次,同个对象的不同实例共用

在方法调用时,系统为分辨调动的主体–因而引入this指针的概念:

2.2 this 指针

编译器对类型的编译为以下三步:
1.识别函数属性
2.函数原型(非函数体部分)的识别
3.改写
如在上例的void studvent::set_age(int age) 会被改写为void studvent::set_age(stuent* const this,int age) ,这样,在多个实例调用方法时会再进行以下改写

int main()
{
  student s1,s2;
  
  s1.set_age(2);//改写为set_age(&s1,2);
 
  s2.set_age(3);//改写为set_age(&s2,2);
}

在c++默认的调用约定thiscall下,this指针的值是通过ecx来传递的,如在s1.set_age(2)中,首先先将s1的地址[s1]传递给ecxlea ecx,[s1],之后进入函数内部,ecx中的值传递给this指针 mov [this],ecx/如使用c的调用约定cdecl,则是以入栈的形式实现push eax/

(在编译时自动入参)
1.是指向本对象的指针;其存在于成员函数内,而非对象内,只有在调用成员方法时产生this指针

2.普通成员方法的第一个参数,默认加上this指针;

3.在普通成员方法内只用到普通成员的地方,加上this指针的解引用 this->
4.在调用成员方法时,加上参数this指针

const与成员方法

如对于成员函数 int get_num()在其之前增加constconst int get_num()是说明该函数返回值为常量;
在其之后增加constint get_num() const则是声明此方法为常方法,常方法内只能对对象进行读取,而无法写入(即无法修改)

注意,常对象如const student s1调用普通方法会报错——普通方法的this指针(student * const this)无法指向常对象(能力扩展)。因此,常对象只可指向常方法。/·普通方法不受限制·/

2.3构造函数

当所有成员属性可见性皆为public时,可直接用{···}对对象进行初始化:

class pointer
{
  public:
     int row;
     int col;
};

int main()
{
   pointer p = {12,23};
}

一个对象的数据成员多为私有的,要对它们进行初始化,必须用一个公有函数来进行。同时这个函数应该在且仅在定义对象时自动执行一次。称为构造函数(constructor)

构造函数的用途:1)创建对象,2)初始化对象中的属性,3)类型转换。

初始化列表
只有构造函数有初始化列表
必须初始化的成员放在初始化列表
在本对象构造之前需要完成的动作必须放在初始化列表中
从const 成员 必须放在初始化列表中
const方法
常对象只能调用常方法(this指针不匹配) – 构造,析构,重载不受影响
常方法中只能调用常方法 – 静态函数不影响

class Int
{
  private:
     int val;

  public:
     Int(){}  //缺省构造函数
     Int(int x)  //构造函数重载
     {
        val= x;
     }

};

int main()
{
   Int p1(1;  // 创建对象并初始化
   Int p2;          //调用缺省构造函数
   Int p3()//无法构造对象,系统理解为函数声明

   p2.Int(1,1)  // 错误,构造函数仅可由系统调用,对象无法调用
   
   int b = 2;
   p1 = b; // 类型转化(隐式转换)
   
}

构造函数的类型转化

类型转化只适用于单参的构造函数

上例中,系统先b的值创建一个临时对象,然后将临时对象的值赋给p1;

要防止隐式转换可以通过在构造函数前添加explicit关键字实现,此时实现类型转换需要用强制类型转换(也称显式转换),如 p1 = (Int)b

构造函数使用时–
1.对象进行构造时时默认调用的函数,在对象生存周期内(由系统)只调用一次
2.函数名与类名一致
3.构造函数无函数返回类型说明;但实际上构造函数有返回值,为其创建的对象
4.可重载
5.未定义时,系统默认生成一个默认构造函数(除 this 指针外,没有参数的构造函数)

拷贝构造函数

1.当使用一个已存在的对象为另一个对象赋值时,自动调用的成员方法
2.如果自己不实现,自动生成一个浅拷贝的等号运算符重载函数
3.防止自赋值
4.防止内存泄漏
5.防止浅拷贝
在建立对象时可用同一类的另一个对象来初始化该对象的存储空间,这时所用的构造函数称为拷贝构造函数(Copy Constructor).
这个拷贝过程只需要拷贝数据成员,函数成员是共用的。

class Object
{
  int value ;
public:
  
  object (int x) : valuc(x) {}
  ~object() {}
  
  //拷贝构造函数
  //使用引用的原因——防止死递归
  //无返回值
  //也可让参数为常对象引用 
  object(object& obj):value(obj.val)
   {}
}

//下例中将构造5次对象
object fun(object obj) //3
{
  int val = obj.Getvalue();
  object obja(val);  //4
  return obja;    //5  构建临时对象——调用拷贝构造
}


int main(
{
object obja(10) ;//1
object objb(0);  //2  调用构造函数
objy = fun(obja);

return 0}

为避免函数的多次构建,可以将参数改为引用形式——即object fun(object& obj) ;

有时,为了避免obj对象被错误修改,也可以加上const关键字—object fun(const object& obj)

1.用一个已存在的对象为另一个正在生成的对象初始化的时候自动调用的成员方法;
2.应当预防浅拷贝
3.如果未自己实现,则生成一个浅拷贝的拷贝构造函数
4.使用引用;以防死递归(重复创建新对象)

总结:

拷贝构造函数的参数——采用引用。如果把一个真实的类对象作为参数传递到拷贝构造函数,会引起无穷递归。在类中如果没有显式给出拷贝构造函数时,则C++编译器自动给出一个缺省的拷贝构造f函数。如果有程序设计者定义的构造函数(包括拷贝构造函数),则按函数重载的规律,调用合适的构造函数。

浅拷贝 – 直接为指针赋值 (不常用)
深拷贝 – 重新申请内存并将数据传入

2.4析构函数

当一个对象的生命周期结束时,C++会自动调用一个成员函数注销该对象,这个成员函数叫做**析构函数(destructor) **

析构函数–
1.对象生存周期满之后系统会自动调用,也可由对象主动调动
2.~ + 对象名;如person类的析构函数为 ~person()
3.在栈帧中,先构造的函数后析构;
4.未实现时,调用默认析构函数(该函数什么都不做)
5.一个类中只有一个析构函数
6.析构函数无函数返回类型说明;是否有返回值取决于编译器

友元

为了在类外访问其内部成员,将要注册的函数/成员/类前添加friend关键字并将其声明添加进类内,使其可访问类的内部成员

友元不具有自反性
友元不具有传递性
友元不具有继承性

实现方式
1.函数友元

class Object
{
private:
    int value;
public:
    0bject(int x) :value(x){}
    friend ostream& operator<<(ostream& out,const Object& obj) //声明为友元函数
};

ostream& operator<<(ostream& out,const Object& obj)
{
  out << obj.value; //友元函数可以访问类的private成员
  return out;
}
int main()
{
Object obja(10);
cout<< obja << endl;
}

2.成员函数友元

class Object; //构造base前需要对object对象声明
class Base
{
private:
  int sum;
public:
  Base(int x = 0): sum(x){}
  void fun (Object&);
};

class 0bject
{
private:
  int value;
  public:
  0bject(int x) : value(x)0
  friend void Base::fun(Object& obj){};//base的成员函数声明为友元函数
};

void Base::fun(Object& obj)
{
  obj.value = obj.value + sum;
}

3.类友元

class Object; //构造base前需要对object对象声明
class Base
{
friend class Object; //Object注册为Base的友元函数
private:
  int sum;
public:
  Base(int x = 0): sum(x){}
  void fun (Object&);
  void print(Object&);
};

class 0bject
{
friend class Base; //Base注册为Object的友元类,base类可以访问Object的成员
private:
  int value;
  public:
  0bject(int x) : value(x)0
};

//函数的类外实现
void Base::fun(Object& obj)
{
···
}

void Base::print(Object& obj)
{
···
}


=运算符重载

在运算符前添加关键词operator

class person
{
	public:
	int age;
	int sex;	
	
	person()
	{
		_age = src._age;
		_sex = src.sex;
	}
	
	//运算符重载
	//以引用返回,因为this的生命周期大于operator=,可以以引用方式返回,从而避免了构建临时对象
	person& operator= (const person& src) const
	{	
	    // 防止自符值
		if (this= &src)
		{  	
	    this->age = src.age; 
        }
        //返回*this,可以实现连续赋值
        //如person1 = per2 = per3
         return *this ; 
	}
}; 


int main()
{
	person p1(32,1);
	person p4;
	
	p4 = p2;//编译如下:p4 = p4.operator(p2);
	       //         operator(&p2,p2);
	
	
	return 0}

()运算符重载

对类型转换符()的重载,能使得对某些class的操作更加方便。
例如:

class Int
{
  private:
     int val;

  public:
     Int(int x):val(x){}
     
     operator int() const
     {
       return val;
     }
};

int main()
{
   Int a(1),b(2);  
   a < b;   //系统此处调用()
   
   int x = 100;
   a < 100; //同上
   
}

例2:

class Add
{
  //易变关键字mutable,使得声明变量在常方法中也可以被改变
  mutable int value;
  public:
  Add(int x = 0:value(x){}
  itn operator()(int a,int b) const
  {
     value = a + b;
     return value;
  }

   
}
int main ({
  int a = 10,b = 20,c = 0;
  Add add;
  c = add (a,b)//仿函数
  //无异于c = add.operator()(a,b);
  return 0;
}

缺省函数

注意,当以下函数未自行构建时,系统会自动提供缺省函数
在这里插入图片描述
在这里插入图片描述

例:

class object
{
private:
   int num;
   int ar[5]:
public:
  object (int n,int val = 0):num(n)
  {
  for(int i - 0: i < n; ++i)
    {
       ar[i] = val;
    }
  }
};

int main()
{
  object obja(5,23);
  object objb(obja);
  object objc(5);
  objc = obja;
}


自建的拷贝与赋值重载如下

//拷贝
object (const object& obj):num(obj.num)
{
  for(int i = 0: i < 5; ++i)
    {
       ar[i] = obj.ar[i];
    }
}
  
 //赋值
 object& operator=(const object& obj)
{
  if(this != &obj)
  {
     num = obj.num;
     for(int i = 0: i < 5; ++i)
     {
        ar[i] = obj.ar[i];
     }
   }
 return* this;
 }

注意:如果未自己构建,且赋值的类为简单类型,系统所构建的缺省函数不会有函数调用过程,而只是简单的赋值。

简单类型:不具有继承关系,不具有虚函数,成员均为基本数据类型

如在 objc = obja中,系统只是获取obja的地址,并将其中的数据依次赋值给objc中对应的空间

例2:

class Object
{
  int num ;
  int* ip;
public:
   Object(int n,int val = 0):num(n)
   {
     ip = (int*)malloc(sizcof(int) *n) ;
     for (int i = 0; i < num; ++i)
     {
        ip[i] = val ;
     }
   }
~Objcct()
  {
    free(ip);
    ip = NULL;
  )
}int main()
{
  Object obja(523);
  Object objb(obja);
  Object objc(8);
  objc = obja ;
}

在该例中,执行 objc = obja,若未构建赋值运算符重载函数,系统会将objc中的num赋值以obja中的num;同时,objc中的ip指针则会被赋值为obja中ip的值(即两个指针指向同一片空间),这并不是我们想要的结果。

因此需要自建赋值运算符重载函数

自建的拷贝与赋值重载如下

//拷贝
object (const object& obj):num(obj.num)
{
     ip = (int*)malloc(sizcof(int)*num) ;
     for (int i = 0; i < num; ++i)
     {
        ip[i] = obj.ip[i];
     }
}

  
 //赋值
 object& operator=(const object& obj)
{
  if(this != &obj)
  {
     num = obj.num;
     ip = (int*)malloc(sizcof(int)*num) ;
     for (int i = 0; i < num; ++i)
     {
        ip[i] = obj.ip[i];
     }
   }
 return* this;
 }

小结:

  • 1.运算符重载函数的函数名必须为关键字operator加一个合法的运算符。在调用该函数时,将右操作数作为函数的实参。
  • 2.当用类的成员函数实现运算符的重载时,运算符重载函数的参数(当为双目运算符时)为一个或(当为单目算符时)没有。运算符的左操作数一定是对象,因为重载的运算符是该对象的成员函数,而右操作数是该函数的参数。
  • 3.单目运算符“++”和“- -”存在前置与后置问题。
    前置“++”格式为:
    class:operator++(){…}
    而后置“++”格式为:
    class:operator++(int){…}
    后置“++”中的参数int仅用作区分,并无实际意义,可以给一个变量名,也可以不给变量名。
    1. 当返回值为自身(*this)时,函数重载以引用方式返回(如=,前置++,+=);返回值为临时量时,以值得形式返回(如+,后置++,)

2.5 C++中的权限

private私有的
类内部可使用,其他地方不可调用
不做设置时,class中权限默认为private

权限选择:对外界必须提供时,定义在public中;其他情况定义在private中;
成员属性定义在private,外部需调用时仅提供接口;为防止改动,接口定义时使用const
struct同样可定义一个类;其中默认权限为public

初始化 –
赋值 –

哪些成员方法写成常方法
1.如果成员方法内不要改动成员,并且没有对外暴露成员引用||指针,就可以写成常方法;
2.如果成员内部不需要改动成员,但是会对外暴露成员引用或是指针;则写成两个成员方法(const方法与非const方法)
3.如果成员方法内部需要改动成员,则写为普通方法

Person()
{
:_sex(1//初始化列表
}

Person(const{
:_sex(1//初始化列表
}

//此const修饰this指针;等效于 const *  this 
Person()const
{
:_sex(1//初始化列表
}

静态成员变量与静态成员方法

  • 静态量存储在数据段
  • 一个类的不同实例共用一个静态成员,并且其不占用类实体的空间
  • 需要在类外进行初始化
  • 必须在类外的.cpp文件中初始化,且只能初始化一次(在.h文件初始化会使每次调用都会初始化)
  • 静态成员方法访问不依赖this指针,
    只能使用静态成员变量(因为其不依赖this指针)
  • 派生类共享使用基类的静态成员变量
template<class T>
class 0bject
{
private:
//常见静态成员可见性设为private
 static int num; //静态成员不能在类内初始化
 int  test=0;
public:
  T value;
  Object(T x = T()): value(x){}
  //静态成员函数,static并非修饰其返回值,而是表面函数不使用this指针,因此该函数只可访问静态成员,并且此函数无法声明为const方法
  static int add()
  {
    num++; //T,静态方法只可访问静态成员
    test++;//F,无this指针无法访问非静态成员
  }

};

template<class T>

int Object<T>::num = 0;//静态成员的类外初始化,注意,初始化语句并非函数内的执行语句,因此,即便 num可见性为private,该语句依然正常执行


class Base : public Object<int>
{
public:
   Base()
   { 
       num += 1;
   }
  void Print() const
  {
    num +=1; //static成员不依靠this指针访问,因此即便方法为常方法,num依旧可以被修改
   cout << "Base:num:" <<num <<endl ; 
   }
};

class Test : public Object<double>
{
public:
   Test() 
  {
      num += 1;
  }
  void Print() const 
 { 
   cout << "Test: " <<num << endl; 
  }
};

int main()
{
  Base b1,b2;
  Test t1,t2;

  b1.print(); //
  t1.print(); //
}

例1:

class Object
{
public :
  int value;
  static Object obj;
};

注意,该class是可以编译通过的,static Object obj只有一份且存在于数据区,为各Object实体所共用,单独的Object实体内只含有value属性,所以不存在重复构建的问题

例2.1:static成员在单例模式中的应用
单例模式:创建对象时,只创建一个对象

class Object{
private:
  int value;
  static Object instance;
  
    //构造函数设为私有,使其无法被外部函数调用
   Object (int x = 0) : valuc(x){}
 //将拷贝构造与=重载函数delete(弃用)
  Object (const Object& obj) = delete;
  Object& operator=(const Object& ob) = delete;
  
public:
//通过一个public函数提供接口来实现外部对类内唯一对象static Object instance的访问
//注意,只能以&的方式返回,因为值返回所使用的拷贝构造函数(返回时构建临时对象会调用拷贝构造函数)已经被delete
  static Object& GotTnstance ()
   {
     return instance;
   }
 };
 
Object Object:: instance(10); //static 对象的类外初始化

int main()
{
  Object obj(10); //F,外部函数不可调用构造函数
  Object& obja = Object::GotTnstance();//T
  Object& bojb = obja.GotTnstance(); //T,obja与objb所引用的是同一个对象
  return 0;
}

此例中所实现的单例模式称为线程安全(对象构建时的线程安全)

三 . 继承与多态

**继承(inheritance)**机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构。体现了由简单到复杂的认识过程。

**多态性(polymorphism)**是考虑在不同层次的类中,以及在同一类中,同名的成员函数之间的关系问题。函数的重载,运算符的重载,属于编译时的多态性(也称为早绑定)。以虚基类为基础的运行时的多态性是面向对象程序设计的标志性特征(也称为晚绑定)。

类与类之间的关系:

  1. 嵌套 --类中声明其他类
  2. 代理 --类的接口时另一个类接口的子集(一个类的功能需要另一个类实现)
  3. 友元 –
  4. 属于 --一个类是另一个类的一部分
  5. 组合 –
  6. 继承 –

3.1继承:

被继承的类称为基类
新产生的类称为派生类

继承时会有基类的属性;
派生类无法访问基类的私有成员;
不论继承权限如何,派生类总能访问隐藏基类对象的public与protected成员;
如在派生类内创建基类对象,则只可访问其public成员

public继承反应了现实中 “is a” 的关系

class fish
{
	public:
	string name;
}

class GoldFish:public fish  //此处的public为继承权限  缺省时为private继承
{
	public:
	string color;
}

int main()
{
	GoldFish gfish;
    gfish.name ="jinyu";
	gfisn.color = "gold";
}
/自身类子类外界
public111
protected110
private100

派生类对象构建时先构建其基类-产生对象时,派生类含有隐藏基类对象

继承权限:
class GoldFish:public fish中的public为继承权限

  • 子类继承的父类成员在自身类内不能高于“继承权限”
  • 子类的构造函数优先构造父类再构造子类
  • 执行子类的析构函数时优先析构子类再析构父类

父类的构造需要传参时,则必须写于初始化列表中

同名问题

属性同名

同名隐藏-若派生类对象某一属性与基类重名,则会隐藏 基类的同名属性;要对其访问需要使用作用域解析符::

class A{
//若声明派生类为基类的友元friend class B;则派生类可访问基类所有成员
protected:
    int ax;
public:
    A() :ax(0){}
};

class B : public A
{
private:
    int ax;
public:
   B():ax(10){}
   void fun ()
{
   ax = 100 ;
   //可以typedef A Base;
   //从而Base::ax = 200;
   A::ax = 200;
}
};

int main()
{
    B b;
    b.fun() ;
}

方法同名

同样对其使用作用域解析符::

class A
{
protected:
    int ax;
public:
    A() :ax(0){}
    void fun ()
{
   ax = 100 ;
   A::ax = 200;
}
};

class B : public A
{
private:
    int ax;
public:
   B():ax(10){}
   void fun ()
{
   ax = 100 ;
   A::ax = 200;
}
};

int main()
{
    B b;
    b.fun() ;
    b.A::fun()
}
  • 注意,仅可在public继承下可实现;private或protected继承下的隐藏基类在外部函数(main)中无法被访问

赋值兼容规则

在任何需要基类对象的地方都可以用公有派生类的对象来代替,这条规则称赋值兼容规则。它包括以下情况:C++面向对象编程中一条重要的规则是:公有继承意味着“是一个”。一定要牢牢记住这条规则。
1.派生类的对象可以赋值给基类的对象,这时是把派生类对象中从对应基类中继承来的隐藏对象赋值给基类对象。反过来不行,因为派生类的新成员无值可赋。
2.可以将一个派生类的对象的地址赋给其基类的指针变量,但只能通过这个指针访问派生类中由基类继承来的隐藏对象,不能访问派生类中的新成员。同样也不能反过来做。
3.派生类对象可以初始化基类的引用。引用是别名,但这个别名只能包含派生类对象中的由基类继承来的隐藏对象。

3.2 多态

多态分为静多态与动多态

相对于发生在编译期的静多态,动多态发生在运行期;这也是两者的主要区别

时期例子
静多态编译期摸板,函数重载
动多态运行期虚函数

动多态

动多态的产生条件:
使用指针或是引用调用虚函数 ,且对象需是一个完整的对象

完整的对象是指 构造函数执行完毕,析构函数未执行的实例

即动多态是通过继承(public继承)、虚函数(virtual)、指针(->& virtual)来实现。(缺一不可)

动多态的调用过程:

  1. 使用指针或者引用调用虚函数
  2. 在对象中找到vfptr
  3. 根据vfptr找到vftable
  4. 在vftable中找到要调用的函数
  5. 调用

虚函数

virtual
虚函数具有传递性

class object
{
public:
   virtual object * fun() 
}


class Base :public Object
{
public:
    virtual Base * fun()
} 
;

vftable(虚表)

· vftable什么时候产生?于何处存储?
编译期;rodata段(只读数据段)

class中有虚函数就会创建虚表;
虚表存储各虚函数的函数指针;

编译时,编译器若发现派生类对象有基类的同名函数,则会发生同名覆盖,虚表中的函数指针被替换;

class Base
{
private: 
    int value;
public:
Base(int x = 0) : value(x){}
virtual void add() {}
virtual void fun() {}
virtual void print() const {}
}

class derive  : public Base
{
private: int sum;
public:
derive  (int x = 0) : base(x+10) , sum(x){}
virtual void add() {}
virtual void fun() {}
virtual void print() const {};

int main()
{
  derive  derive  (10);//大小为12个字节,8(sum,value)+ 4 (虚表指针vfptr)
}

为了调用虚表中的函数指针,每个有虚函数的类都会额外开辟4个字节来存储一个虚指针_vfptr,用其来索引向自己类的虚表;

虚表指针_vfptr

_vfptr在构造时候写入对象的存储空间,其用来指向该类的vftable

一个类的虚表只有一份
虚表指针存在于派生类对象的隐藏基类中

如在上例中,创建derive对象时,首先调用base的构造函数创建隐藏base类;因其有虚函数,编译器同时创建虚表指针_vfptr,使其指向base的虚表;之后构建derive,对所有vftable中的同名函数进行同名覆盖,同时,该派生类中的虚表指针改为指向derive的虚表;

即-父类中的虚函数会被子类中相同的函数覆盖;该过程发生于子类在构建时的虚函数表中

  • 什么情况下析构函数需写成需虚函数?
    当存在父类指针指向堆上的子类对象时,则需把父类的虚构函数写成虚函数**

  • 构造函数能不能写成虚函数
    不能,构造函数是虚表创建的前提,而virtual函数的构造需要用到虚表

  • 静态函数能不能写成虚函数
    不能;静态函数不依赖于对象,从而无法产生动多态

  • 析构函数能不能写成虚函数
    能,当基类析构函数声明为虚函数时,其派生类析构函数自动带有virtual声明
    析构函数会reset虚表,当derive的析构函数执行完成后,_vfptr会重置指向base的虚表,从而执行base的析构函数;

  • 虚函数能否写为内敛函数
    不能,虚函数在编译期需要将函数指针放入vftable;内敛函数在编译期展开;在release版本中没有地址

类的编译顺序
先编译类名
再编译成员名
再编译成员函数

RTTI

vftable = RTTI + 函数指针


int main()
{
	
	
	//dynamic_cast<type_name> 父类指针强转子类指针的专用类型指针,
	//于vftable的RTTI中寻找type_name类型的
	//1.必须有RTTI,2.父类指针指向的对象中的RTTI确实是子类
	Derive *pd = dynamic_cast<Derive*>(p);
	
	return 0;
}

菱形继承与虚继承

菱形继承
——该继承会导致造成公共基类在派生类对象中存在多个实例
在这里插入图片描述

使用虚继承来解决菱形继承问题

class Object
{
int value;
public:
Object(int x = 0) : value(x){}
};

class derive: virtual public Object//虚继承
{
int num;
public:
Base(int x = 0) : num(x)Object(x + 10){}
};

class Test :virtual public Object
{
int sum;
public:
Test(int x = 0): sum(x)Object(x + 10){}
};

class Det : public base,public Test 
{
private:
int total;
public:
Det(int x=0):total(x),base(x+10),Test(x+20),Object(x+100){}
};

int main()
{
  det d(0);
  return 0;
}

d1内存分配图如下
在这里插入图片描述
object首先被创建

被虚继承的类称为虚基类;

虚基类在派生类对象中存放于vbtable中;

虚基类在派生类中被构造时,在原本存储该基类对象的位置上创建一个指针,来指向虚基类实例的位置;

从而保证虚基类在派生类中只会有一个实例存在

虚基类在派生类构造时会被直接当作父类继承

纯虚函数与抽象类

virtual void add() = 0;
base的纯虚函数实现依靠derive;
纯虚函数是为给派生类提供接口;

有纯虚函数的类叫做抽象类
抽象类不能用来实例化对象,出于该目的,也常将抽象类的构造函数声明为protect权限;

要求限制子类必须覆盖某个接口


class A
{
public:
    virtual void fun() = 0;  //纯虚函数
}

int main
{
   A a;
   
}
  • 9
    点赞
  • 105
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值