栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

C++学习笔记

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

C++学习笔记

C++学习笔记

一 .C与C++区别

1.1 const

const与指针 1.2 引用(别名)

*引用与const 1.2 默认值参数1.3 内联函数1.4 函数重载

1.2.1 C与C++函数的互相调用 1.5 函数摸板

数组引用与函数模板 1.6 new与malloc1.7命名空间1.8 右值引用

*右值引用与将亡值右值引用应用右值引用与函数模板 二.面向对象

2.1 面向对象2.2 this 指针

const与成员方法 2.3构造函数

构造函数的类型转化 拷贝构造函数2.4析构函数友元=运算符重载()运算符重载

缺省函数 C++中的权限静态成员变量与静态成员方法 三 . 继承与多态

继承:

同名问题

属性同名方法同名 赋值兼容规则 多态

动多态虚函数vftable(虚表)

虚表指针_vfptr RTTI

菱形继承与虚继承 纯虚函数与抽象类 四.STL容器

近容器顺序容器

vectorlist双向链表dequeue 双端队列 容器适配器关联容器

setmap 4.2 STL 迭代器

顺序迭代器插入迭代器流迭代器智能指针auto_ptr(于c++17中被舍弃)强智能指针

一 .C与C++区别

cpp= c + class + stl + template

1.1 const

C语言中
const int a = 1 ;
设a为常变量 其值在某些情况下可被修改:

  const int b = 10;
  int* p = (int)&b;
  *p = 20;
 printf("b = %d  ,*p = %d n"); // 结果为 b = 20, *p = 20;

C++ 中
const声明即为常量,其值不可被修改;

常量:在编译期将常量的值直接写入到常量的使用点;因此常量必须初始化。(在c++编译规则中)

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

int b = 1;
const int c = b;//此时c为常变量
const与指针
int a =10;

const int * p1 = &a; //const修饰 *(即指向),因此不可通过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; 

当int * p 被const修饰时——

//例1

int a =10;
//通过P对a只可读而不可写
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的指向不可改变
const int * 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.2 默认值参数

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

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

 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.3 内联函数

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版本不生成符号,直接在调用点展开。

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

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

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

1.4 函数重载

c++以函数原型来区分不同函数

函数的原型 包括函数返回类型,函数名,形参列表

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

int Max(int a,int b);
double Max(double a,double b); 

不以返回类型区分的原因:
调用时产生二义性
即在调用时各函数均符合其调用规则,此时便无法调用

int Max(int a,int b);
double Max(int a,int 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;
}

函数重载是在编译时期决定的调用哪一个函数——静多态的一种

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

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

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

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

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

1.5 函数摸板
//函数模板

//此处T仅作标识符用,可替换为任意字符
//也可写作 template
template
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);
   swap(da,bb);
   swap(ch1,ch2);
 //  自定类型(struct)也可以调用函数摸板
}

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

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

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

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

类型转换的实现并非为宏的替换-swap(int& a,int& b)
而是使用类型重命名规则
typedef int Type
void swap (Type& a,Type& b)

数组引用与函数模板

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

int main()
{
    ip = new int // 在堆区申请一个整形空间
    *ip = 100;
    delete 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.7命名空间

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(); 
}

1.8 右值引用

左值 lvalue:可被寻址的值

右值 rvalue:不可被寻址的值

将亡值字面常量

int main 
{
int a =10;//a为左值,因其可寻址;反之10为右值
const int b = 20;//b为左值,其可寻址;

int& c = a;  //正确
int& d = 10; //错误,左值无法引用右值
}

如何实现对常量的引用?

const int& a = 20;//1.使用常引用
//其底层实现如下:  int tmp = 10;
//    const int* const a = &tmp; 

int&& b = 10;//2.使用右值引用 
//其底层实现如下:
//int tmp = 10;
//int* const b = &tmp;
//在理解上,int&& b = 10 与int b = 10无异

*右值引用与将亡值

提高性能

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

int main()
{
int x = fun ();// T
//fun()return时,a的值存入临时量,回到主函数时,临时量的值赋给x;

int& b = fun() ;// F

const int& c = fun();//T
//编译器判断接收值为常引用,会将a的值创建在主函数空间,并使c直接引用该空间,该值不可改

int&& d = fun () ; // T
//将亡值的创建同上,d的值可改
return 0;

上例中,d用右值引用获取将亡值,会延长其将亡值的生存周期,等同于d的生存周期

右值引用应用

右值引用主要用以处理内置类型的将亡值

内置类型(char,int,double)与自设类型(class)处理方式不同,因此优化方式不同

struct类型的处理方式介于 内置 与 自设 之间

class 构建的对象 = 方法 + 数据struct 构建的对象 = 数据 / 方法struct内只有数据类型时,其处理方法与内置类型相同

void fun(int& a)
void fun(const int& a)
void fun(int&& a)


int main ()
{
int a = 10;
const int b = 20 ;
const int& x = b;
const int& y = 20;

fun(a); //调用void fun(int& a)
fun(b); //调用void fun(const int& a)
fun(10); //调用void fun(int&& a)
}

右值引用与函数模板
void fun(int& a)
void fun(const int& a)
void fun(int&& a)

template
void fac(T&& a)
{
  fun(a);
}

int main ()
{
int a = 10;
const int b = 20 ;
const int& x = b;
const int& y = 20;

fun(a); //调用void fun(int& a)
fun(b); //调用void fun(const int& a)
fun(10); //调用void fun(int& a)
}

二.面向对象 2.1 面向对象

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

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

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

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

类的设计:

//class为数据类型说明符
class student
{
  public:
     bool sex;
     void set_age(int age);
  private:
      int 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.析构函数无函数返回类型说明;是否有返回值取决于编译器

友元 =运算符重载

在运算符前添加关键词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(5,23);
  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仅用作区分,并无实际意义,可以给一个变量名,也可以不给变量名。

    当返回值为自身(*this)时,函数重载以引用方式返回(如=,前置++,+=);返回值为临时量时,以值得形式返回(如+,后置++,)
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指针)派生类共享使用基类的静态成员变量

templateclass 0bject
{
T value;
public:
Object(T x = T()): value(x){0}
static int num;
};
template
int Object : : num = 0;


class base : public Object
{
public:
base() { num t= 1; }
void Print ( const i cout < base:numen
i"Rase: " <{
public:
Test() { num t= 1 ; }
void Print() const { cout << "Test: " < 
三 . 继承与多态 

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

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

类与类之间的关系:

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

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

继承时会有基类的属性;
派生类无法访问基类的私有成员;
不论继承权限如何,派生类总能访问隐藏基类对象的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.派生类对象可以初始化基类的引用。引用是别名,但这个别名只能包含派生类对象中的由基类继承来的隐藏对象。

多态

多态分为静多态与动多态

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

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

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

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

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

动多态的调用过程:

    使用指针或者引用调用虚函数在对象中找到vfptr根据vfptr找到vftable在vftable中找到要调用的函数调用
虚函数

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 父类指针强转子类指针的专用类型指针,
	//于vftable的RTTI中寻找type_name类型的
	//1.必须有RTTI,2.父类指针指向的对象中的RTTI确实是子类
	Derive *pd = dynamic_cast(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;
   
}
四.STL容器 近容器

string
常用的操作:
push_back(); 尾插
pop_back(); 尾删
size(); 获取大小
erase(); 删除
back(); 获取尾元素
front(); 获取头元素
find(); 查找 元素/子串 (返回值为目标下标,未找到返回-1)
clear(); 清空
empty(); 判空
copy(); 从对象拷贝到指针
reserve(); 更改vector的容量,使其至少可以容纳n个元素
swap(); 交换两个容器内的数据

int main()
{
	
	string s1("s");//构造
	
	string s2(s1.begin(),s1.end());//产生新对象,内容为迭代器区间的内容
	
	 // 使用迭代器遍历
	string::iterator it = s.begin(); 
	for(;it != s.end();it++)
	{
			cout<<*it< 
顺序容器 
vector 

vector —— 一维数组
{
push_back(); 尾插
pop_back(); 尾删
insert(); 在指定位置插入
size(); 获取当前数据个数
max_size(); 获取最大容量
erase(); 删除
back(); 获取尾元素
front(); 获取头元素
//find(); 顺序容器无
clear(); 清空
empty(); 判空
copy(); 从对象拷贝到指针
resize(); 调整容器的长度大小,使其能容纳n个元素。如果n小于容器的当前的size,则删除多出来的元素。
reserve(); (以1.5倍扩容)
swap(); 交换两个容器内的数据

}

使用下标遍历

	for (int i = 0; i 

使用迭代器遍历

    vector::iterator it = v.begin();
	for (; it != v.end(); it++)
	{
		cout << *it << " ";
	}
list双向链表

list
{
push_front(); 头插
pop_front(); 头删
reverse(); 逆置
//reserve(); 链式结构,无扩容
其他操作与vector一致;
}

不同容器间的初始化

list l;
//当两容器内数据类型相同时 用此方法初始化
vector v1(l.begin(),l.end()); 
dequeue 双端队列

dequeue 双端队列 二维数组
{

}

int main()
{
vector v;
for
}

容器适配器

stack – deque
{
push();
pop();
top();
empty();
size();

}
queue – 底层由deque实现
{
push();
pop();
front();
empty();
size();
}

priority_queue 优先级队列

关联容器 set

set 集合 数据有序且不允许重复 底层实现为红黑树(因此有序且增删改查时间复杂度低)
{
insert();
find(); //对于关联容器,使用find()效率更高 查找失败时返回end();
count(i); //元素i出现的次数
}
multiset 可重复

map

map 映射表 数据按key有序 红黑树 不允许重复
{
make_pair(1,“s”);
pair(1,“s”)
}
输出
cout<< it->first << ;
cout<< it->secend << ;

用 [ i ]访问map时 ;返回键值 i 对应的值;若没找到则返回空

empty();
clear();
srase();
count(i); 计算键值 i 出现的次数
size();

multimap 可重复

4.2 STL 迭代器 顺序迭代器

vector

iterator 正向迭代器
{
begin(); // 指向首元素
end(); // 指向尾元素之后
}

reverse_iterator 反向迭代器
{
rbegin(); // 指向首元素之前
rend(); // 指向尾元素
}

const_reverse_iterator 正向常量迭代器
{
crbegin(); // 指向首元素之前
crend(); // 指向尾元素
}

const_reverse_iterator 向常量迭代器

插入迭代器 流迭代器 智能指针auto_ptr(于c++17中被舍弃)
class my_auto_ptr
{
private:
      bool _Owns;
      T* _Ptr;
          

}

强智能指针

1.不允许隐式构造
2.不能使用同一个裸指针初始化多个智能指针
3.允许拷贝构造,不允许=运算符重载
shared_ptr s_p(new int(11))

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/743811.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号