C++

命名空间

namespace:命名空间 区分同一个作用域下的相同成员

:: 作用域运算符

int AA = 20;
int main(){
	int A = 10;
	cout<<A<<endl;  //10
	cout<<::A<<endl;  //20  :: 前面不指定作用域,代表取全局的作用域
}

命名空间使用两种方法:
1.手动打开命名空间,注意名称冲突问题(命名空间中变量名称冲突)

2.显示的指定具体的命名空间,每次使用都需要指定

namespace AA{
	int A = 20;	
}
namespace BB{
	int A = 10;
}
using namespace BB;
int A = 30;
int main(){
	//cout<<A<<endl;  //不明确,报错
	cout<<::A<<endl;  //30  :: 前面不指定作用域,代表取全局的作用域
	cout<<AA::A<endl;  //20  显示的指定要使用的命名空间
	cout<<BB::A<endl;  //10
}

bool

C:

#include<windows.h>
typedef int BOOL;  //给int类型起的别名
#define TRUE 1  //宏
BOOL b = TRUE;  //FALSE 0
cout<<b;  //1
cout<<sizeof(b);  //4

C++:

bool bb = true;  //bool true false关键字,也是C++内置类型
bb = false;
cout<<bb;  //0
cout<<sizeof(bb);  //1

bool 与 BOOL 的区别:
1.typedef int BOOL; //给 int 类型起的别名,bool true false 关键字,也是C++内置类型

  1. 定义变量所占用的空间不一样

string

char szArr[10]="asdasdasd";  //栈区
char *p1 = "123123123";  //指针变量指向字符常量区
//szArr = "123123123";  //szArr是数组首元素地址
p1 = "asdasdasd";  //改变的是一个指向
strcpy_s(szArr,10,"qweqweqwe");
cout<<szArr<<"   "<<p1<<endl;  //qweqweqwe asdasdasd
char* p2 = new char[10];
//p2 = "zxczxczxc";不可以这样写 p2指向字符常量区后不能delete对应字符常量区的内容 产生报错
strcpy_s(p2,10,"qweqweqwe");
delete []p2;

#include<string>
using namespace std;
string str = "123";
cout<<str<<endl;  //123
str = "456";
cout<<str<<endl;  //456
string str1 = "qwe";  //字符串拼接
str1+=str+"----";
cout<<str1<<endl;  //qwe456----
string str2 = str1.substr(2,4);  //截取字符串 返回值:string类型的字符串 参数:开始位置(从0开始的下标),截取多少个字节
cout<< str2<<endl;  //e456

string str = "123";
cout<<str<<endl;  //123
string str1 = str;
cout<<str1<<endl;  //123
if(str1 == str2){  //相等
	cout<<"相等"<<endl;
}else{
	cout<<"不相等"<<endl;
}
void show(const char *p)
{
	cout<<p<<endl;
}
show(str1.c_str());  //string  -> const char*  -> char* 123
char *p1 = "zxc";
string str4 = p1;
cout<<str4<<endl;  //zxc

nullptr

//nullptr;   //关键字
#ifndef NULL
#ifdef __cplusplus
#define NULL    0
#else
#define NULL    ((void *)0)
#endif
#endif
1. NULL 是一个宏,在C++ 替换的0,nullptr 关键字 
2. NULL 在C++ 是一个整型数字,nullptr 空指针
void show(char * p){
	//cout<<p<<endl;
	cout<<"show(char * p)"<<endl;
}
void show(int * p){
	//cout<<p<<endl;
	cout<<"show(int * p)"<<endl;
}
void show(int a){
	cout<<"show(int a)"<<endl;
}
int *p  = NULL;
cout<<p<<endl;  //00000000
int *p1 = nullptr;  //空指针
cout<<p1<<endl;  //00000000
show(NULL);  //int a
//show(nullptr);  //区分指针和int 但是不能区分char *和int *
show((char*)nullptr);  //char *
how((int*)nullptr);  //int *

对象的大小

如果什么都没有 空类---1

空类大小 1,占位作用标识当前对象真实存在与内存中

空类的大小 在C++中空类会占一个字节,这是为了让对象的实例能够相互区别。 具体来说,空类同样可以被实例化,并且每个实例在内存中都有独一无二的地址,因此,编译器会给空类隐含加上一个字节,这样空类实例化之后就会拥有独一无二的内存地址。

成员变量属于对象,对象有多少份,就存在多少份,定义对象的时候存在

this指针

this 指针:类中非静态成员函数都会有一个隐藏的参数(编译器默认加上的),this 指针

this指向 调用该函数的对象, 连接 成员属性和成员函数的桥梁

类之间的横向关系

类之间的关系大致分两类: 横向 - 纵向
横向:

  1. 组合(复合):包含与被包含,部分与整体,有生命周期约束关系
  2. 依赖: 没有生命周期约束关系
  3. 关联: 有或者没有都能完成某个功能。没有生命周期约束关系
  4. 聚合: 有生命周期约束关系

类间关系有很多种,在大的类别上可以分为两种:纵向关系、横向关系。

纵向关系就是继承关系,它的概念非常明确,也成为OO的三个重要特征之一.

横向关系较为微妙,按照UML的建议大体上可以分为四种:

  1. 依赖 (Dependency)

  2. 关联 (Association)

  3. 聚合 (Aggregation)

  4. 组合 (Composition)

它们的强弱关系是没有异议的: 组合>聚合>关联>依赖

  1. 依赖:

UML表示法:虚线 + 箭头

关系:" ... uses a ..."人需要空气

此关系最为简单,也最好理解,所谓依赖就是某个对象的功能依赖于另外的某个对象,而被依赖的对象只是作为一种工具在使用,并不持有对它的引用。

2.关联: 朋友的平等关系

UML表示法:实线 + 箭头

关系:" ... has a ..."

所谓关联就是某个对象会长期的持有另一个对象的引用,而二者的关联往往也是相互的。关联的两个对象彼此间没有任何强制性的约束,只要二者同意,可以随时解除关系或是进行关联,它们在生命期问题上没有任何约定。被关联的对象还可以再被别的对象关联,所以关联是可以共享的。

  1. 聚合: 所属关系

UML表示法:空心菱形 + 实线 + 箭头
关系:" ... owns a ..."

聚合是强版本的关联。它暗含着一种所属关系以及生命期关系。被聚合的对象还可以再被别的对象关联,所以被聚合对象是可以共享的。虽然是共享的,聚合代表的是一种更亲密的关系。

  1. 组合:

UML表示法:实心菱形 + 实线 + 箭头
关系:" ... is a part of ..."

组合是关系当中的最强版本,它直接要求包含对象对被包含对象的拥有以及包含对象与被包含对象生命期的关系。被包含的对象还可以再被别的对象关联,所以被包含对象是可以共享的,然而绝不存在两个包含对象对同一个被包含对象的共享。

// 鼓掌 - 组合功能
void clap(){
	m_hand.HandMove();
	cout<<"发出 阵阵掌声"<<endl;
}
//编程 - 依赖功能
void Code(CComputer * pComputer){
	if(pComputer==nullptr){
	return ;
	}
	m_hand.HandMove();
	cout<<"敲击键盘,输入代码"<<endl;
	pComputer->Coding();
}
// 玩游戏 - 关联功能
void PlayWangzhe(){
	if(m_pFri){
	m_pFri->PlayGame();
	cout<<"我和朋友一起组队玩王者,victory"<<endl;
	}else{
	cout<<"我自己玩王者,拿下五杀"<<endl;
	cout<<"victory"<<endl;
	}
}
//----聚合-------------------------------
	CFamily  family;
	CPeople *pPeo3 = new CPeople;
	CPeople *pPeo4 = new CPeople;
	CPeople *pPeo5 = new CPeople;
	CPeople *pPeo6 = new CPeople;
	family.m_family[0] = pPeo3;
	family.m_family[1] = pPeo4;
	family.m_family[3] = pPeo5;
	family.m_family[4] = pPeo6;
	CComputer *pComp = new CComputer;
	family.AllPeopleCode(pComp);
	delete pComp;
	pComp = nullptr;

函数指针

函数指针:将完成同一功能的不同模块统一标识起来,使结构更加清晰、后期代码更容易维护。
便于分层设计、利于系统抽象,降低代码耦合度使接口和实现分开,提高代码复用性、扩展性。

void show(int a,char *p){
	cout<<"show(int a,char *p)"<<endl;
	cout<<a<<"   "<<p<<endl;
}
void (*p2)(int a,char *p);
p2 = &show;   //可读性高 最好不要直接p2 = show;
(*p2)(10,"123");

用typedef优化定义函数指针

//typedef  给一个类型起别名
typedef int*  AAA;
typedef int BBB;
typedef void (*P_FUN)(int a,char *p);
P_FUN p_fun =&show;
(*p_fun)(20,"456");

//函数
typedef  void (FUN2)(int ,char *);
FUN2* p_fun2 = &show;
(*p_fun2)(20,"456");
void GetShow(P_FUN p_fun){  //接口
	(*p_fun)(20,"123");  //调用实现
}
typedef  void (*P_PARAM)(char *,long);
typedef  long * P_PARAM2 (char*,bool);
typedef  void (*P_RET)(int *,P_PARAM2);
void (*p_fun11    (char*,void (*)(char *,long))   )(int *,long * (char*,bool)){
	return nullptr;
}
/*函数名:p_fun
参数:(char*,void (*)(char *,long)) 
返回值:void (*)(int *,long * (char*,bool))*/
void (*)(int *,P_PARAM2)
P_PARAM param = nullptr;
P_RET ret = p_fun11("123",param);

类成员函数指针

// 无法从“void ( CTest::* )(char *)”转换为“void (*)(char *)”
//类成员函数指针   ::* 是一个整体的操作符,定义类成员函数指针
void (CTest ::* p_fun)(char* str) = &CTest::show;
CTest tst;
(tst.*p_fun)("123");  //   .* , ->*   是一个整体操作符号,作用是调用类成员函数指针,间接引用->指向的函数
CTest *pTest = new CTest;
(pTest->*p_fun)("456");

typedef void (CPeople::*P_FUN)();
CPeople * p = new CBlackPeople;
void (CPeople::*p_fun)() = (void (CPeople::*)())&CBlackPeople::play;  //需要强转
P_FUN p_fun  =(P_FUN) &CBlackPeople::play;
(p->*p_fun)();

void GetPlay(CPeople *pPeo,P_FUN p_fun){
	(pPeo->*p_fun)();
}
CPeople *pYellow = new CYellowPeople;
GetPlay(pYellow,(P_FUN)&CYellowPeople::play);
CPeople *pWhite = new CWhitePeople;
GetPlay(pWhite,(P_FUN)&CWhitePeople::play);

头文件重复包含

解决头文件重复包含的问题:
1.#pragma once : 基于编译器,由编译器去保证

  1. ifndef #define #endif 基于宏判断:逻辑判断,大量文件编译时长会增加,

    宏的名字可能会重复
#pragma once  //告诉编译器,当前这个头文件在源文件中只包含(#include)一次
#ifndef    __TEST_H__
#define    __TEST_H__
//中间为头文件内容
#endif  //__TEST_H__

// 宏: 程序编译时替换
#define   AA   10
//  \  :链接当前和下一行  ,最后一行一般不加 \,  后面不能加任何东西(空格,tab ,注释)
#define BB  for(int i=0;i<AA;i++)\
			{\
				cout<<i<<"    ";\
			}
// 宏可以有参数,参数也是一个替换的作用
#define CC(PARAM)\
for(int i=0;i<PARAM;i++)\
{\
	cout<<i<<"    ";\
}
// # 转成字符串
#define DD(PARAM)\
	cout<<#PARAM<<endl;  //cout<<"12"<<endl;
//## 连接
#define EE(PARAM)\
	cout<<"QQ"##PARAM<<endl;
#define FF(PARAM_CLASS,PARAM_OBJ)\
	PARAM_CLASS PARAM_OBJ;\
	PARAM_OBJ.show();
/*CAA aa;
	aa.show();*/
	FF(CAA,aa)
#define GG(PARAM_CLASS)\
	PARAM_CLASS obj##PARAM_CLASS;\
	obj##PARAM_CLASS.show();
GG(CAA)

重载操作符

重载操作符:本质上是一个函数,告诉编译器当遇到该操作符时,调用当前这个函数
来行使操作符的功能,一般是有返回值的,方便和后续的操作符继续操作。

分两类:类内 、类外

类内,相对类外重载会少一个参数,这个参数在类内与 this 对应。
相当于对象去调用这个重载的函数,一般情况下对象放在左边。

class CTest{
public:
	int m_a;
public:
	CTest(){
		m_a = 10;
	}
	int operator+(/* CTest * const this ,*/int a){
		return this->m_a +a;
	}
	int operator=(int b){
		m_a = b;
		return m_a;
	}
};
CTest tst;
int a = tst+20;
cout<<a<<endl;
int b = tst.operator+(30);
cout<<b<<endl;
CTest tst2;
tst2 = tst = 40;
cout<<tst.m_a<<endl;  //40
cout<<tst2.m_a<<endl;  //40
//40 = tst;
//20+tst;

重载操作符

类内:

int operator+=(int a){
	return m_a+=a;
}
//左 ++
int operator++(){
	return ++m_a;
	//return m_a;
}
// 右++ ,为了区分左++ 右++,重载++时额外指定一个int类型参数,代表右++
//必须用int类型参数 不能用其他的
int operator++(int){
	return m_a++;
}
// 乘法
int operator*(int a){
	return m_a*a;
	}
// 间接引用
int operator*(){
	return *m_p;
}
  1. 重载操作符不能改变操作符本身的用法(参数数量、类型)
  2. 重载操作符 不能改变优先级、结合性。
  3. 重载操作符 不能指定默认参数。
  4. 有些操作符只能在类内 operator= ,operator->,operator[]
  5. 不能重载的操作符,sizeof ? : .

类外:

int  operator+(CTest & tst, int a){

	return tst.m_a +a;
}
int  operator+( int a,CTest & tst){
	return tst.m_a +a;
}
// 左 ++
int operator++(CTest &tst){
	return ++tst.m_a;
}
// 右 ++
int operator++(CTest &tst,int){
	return tst.m_a++;
}
int operator+(CTest &tst1,CTest &tst2){
	return tst1.m_a+tst2.m_a;
}
int operator+(CTest &tst1,CTest2 &tst2){
	return tst1.m_a+tst2.m_a;
}
ostream& operator<<(ostream& os,CTest &tst){
	os<<tst.m_a;
	return os;
}
istream& operator>>(istream& is,CTest &tst){
	is>>tst.m_a;
	return is;
}

仿iterator

在C++ 中 结构体 和类的区别:

  1. 默认的访问修饰符不同,默认 class-private struct-public
    2. 默认的继承方式不同 ,默认 class-private继承 struct-public继承

迭代器 :遍历容器

class CMyIterator{
public:
	Node *m_pTemp;
public:
	CMyIterator(){
		m_pTemp=nullptr;
	}
	CMyIterator(Node * pNode){
		m_pTemp=pNode;
	}
	~CMyIterator(){
	}
public:
	Node * operator=(Node * pNode){
		m_pTemp = pNode;
		return pNode;
	}
	bool operator==(Node  * pNode){
		return m_pTemp==pNode?true:false;
	}
	bool operator!=(Node  * pNode){
		return m_pTemp!=pNode?true:false;
	}
	int operator*(){
		if(m_pTemp){
			return m_pTemp->val;
		}
		return -1;  //返回-1 代表一个标志
	}
	// 左++ 
	Node* operator++(){
		if(m_pTemp){
			m_pTemp = m_pTemp->pNext;
			return m_pTemp;
		}
		return nullptr;
	}
	// 右++ 
	Node* operator++(int){
		if(m_pTemp){
			Node *pTemp = m_pTemp;
			m_pTemp = m_pTemp->pNext;
			return pTemp;
		}
		return nullptr;
	}
};
void ShowList(){
		if(m_pHead){  //如果链表非空
			//Node *pTemp = m_pHead;
			CMyIterator ite(m_pHead);
			while(ite!=nullptr){
				cout<<*ite <<"   ";
				ite++;
			}
			cout<<endl;
		}else{
			cout<<"链表为空"<<endl;
		}
	}

C++简单程序设计

基本数据类型和表达式

1.基本数据类型

bool/char/short/int/long/float/double

2.常量

3.变量

变量作用域

  • 全局变量:具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包含全局变量定义的源文件需要用extern 关键字再次声明这个全局变量。
  • 局部变量:只有局部作用域,它是自动对象(auto),它在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回。
  • 静态全局变量:具有全局作用域,它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被static关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。
  • 静态局部变量:具有局部作用域,它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。

变量内存分配

全局变量,静态局部变量,静态全局变量都在静态存储区分配空间,而局部变量在里分配空间。

4.运算符与表达式

  1. 算术运算符与算术表达式
  2. 赋值运算符与赋值表达式
  3. 逗号运算和逗号表达式
  4. 逻辑运算和逻辑表达式
  5. 条件运算符和条件表达式
  6. sizeof运算符
  7. 位运算
  8. 运算符优先级
  9. 数据类型转换

数据的输入与输出

常用的IO流类库操作符,在头文件iomainp中

  • dec:数值数据用十进制表示
  • hex:十六进制
  • oct:八进制
  • ws:提取空白符
  • endl:换行符,并刷新流
  • ends:插入空字符
  • setsprecision(int):设置浮点数的小数位数,包括小数点
  • setw(int):设置域宽

基本控制结构

  1. 用if实现选择结构
  2. 多重选择结构
  3. 循环语句
  4. 循环结构与选择结构嵌套
  5. 其他控制语句

自定义数据类型

  1. typedef声明:将一个标识符声明成某个数据类型的别名。将这个标识符当数据类型使用

  2. enum枚举类型:

    声明形式:enum 枚举类型名 {变量值列表}

    对枚举元素不能赋值,具有默认值0,1,2,3 ·····,也可以在声明时另行定义枚举元素的值

    枚举类型可以赋值给整值型,整数值不能直接赋值给枚举类型,需要强制类型转换

带默认形参的函数

1.有默认值的形参必须在形参列表的最后: 函数调用中,实参与形参按从左到右的顺序建立对应关系
2.相同作用域内,不允许在同一个函数的多个声明中对同一个参数的默认值重复定义,值相同也不行
3.函数在定义之前有原型声明,默认形参值在原型声明中给出,定义中为了清晰可用注释形式/* */

函数重载

1.定义: 在同一作用域下两个以上的函数,具有相同函数名,但形参个数或类型不同
2.注意: 编译器不以形参名和返回值来区分函数,不要将功能不同的函数定义为函数重载

3.当使用默认形参值的函数重载形式时,需要防止二义性

C++文件编译与执行的四个阶段

1)预处理:根据文件中的预处理指令来修改源文件的内容

2)编译:编译成汇编代码

3)汇编:把汇编代码翻译成目标机器指令

4)链接:链接目标代码生成可执行程序

类和对象

类成员的访问控制

  • public:公开,可以在类外访问
  • protected:类外不可直接访问,派生类(子类)可以访问
  • private:保密,内部所能看见,类外不可直接访问

对象

定义:类的某一特定实例

注意:对象的内存空间只存放数据成员,函数成员不在对象中存储副本,每个函数的代码在内存中只占据一份空间

const

用来修饰内置类型变量,自定义对象,成员函数,返回值,函数参数。

const 修饰普通类型的变量

定义了就要初始化,初始化之后就不能改变其值

const修饰指针变量

  • const 修饰指针指向的内容,则内容为不可变量。// const int *p = 8;
  • const 修饰指针,则指针为不可变量。// int* const p = &a
  • const 修饰指针和指针指向的内容,则指针和指针指向的内容都为不可变量。 //const int * const p = &a;

const 参数传递和函数返回值

  • 值传递一般不加const,函数会自动创建临时变量复制实参值。
  • const参数为指针时,可以防止指针被意外篡改
  • 自定义类型的参数传递,需要临时对象复制参数,对于临时对象的构造,需要调用构造函数,比较浪费时间,因此我们采取 const 外加引用传递的方法。

const 修饰类成员函数

其目的是防止成员函数修改被调用对象的值,如果我们不想修改一个调用对象的值,所有的成员函数都应当声明为 const 成员函数。const修饰this指针所指向的内容,保证了这个const成员函数的对象在函数内部不会发生改变

注意:const 关键字不能与 static 关键字同时使用,因为 static 关键字修饰静态成员函数,静态成员函数不含有 this 指针,即不能实例化,const 成员函数必须具体到某一实例。

const对象不能调用非const成员函数;const成员函数不能调用其他非const成员函数

#define 和const 的区别

1)#define定义的常量没有类型,所给出的是一个立即数;const定义的常量有类型名字,存放在静态区域

2)处理阶段不同,#define定义的宏变量在预处理时进行替换,可能有多个拷贝,const所定义的变量在编译时确定其值,只有一个拷贝。

3)#define定义的常量是不可以用指针去指向,const定义的常量可以用指针去指向该常量的地址

4)#define可以定义简单的函数,const不可以定义函数

static

static 是 C/C++ 中很常用的修饰符,它被用来控制变量的存储方式和可见性。

为什么要使用static?

在函数内部定义的变量,当程序执行到它的定义处时,编译器为它在栈上分配空间,函数在栈上分配的空间在此函数执行结束时会释放掉,这样就产生了一个问题: 如果想将函数中此变量的值保存至下一次调用时,如何实现? 最容易想到的方法是定义为全局的变量,但定义一个全局变量有许多缺点,最明显的缺点是破坏了此变量的访问范围(使得在此函数中定义的变量,不仅仅只受此函数控制)。static 关键字则可以很好的解决这个问题。另外,在 C++ 中,需要一个数据对象为整个类而非某个对象服务,同时又力求不破坏类的封装性,即要求此成员隐藏在类的内部,对外不可见时,可将其定义为静态数据。

静态数据的存储

在全局(静态)存储区

img

在 C++ 中 static 的内部实现机制:静态数据成员要在程序一开始运行时就必须存在。因为函数在程序运行中被调用,所以静态数据成员不能在任何函数内分配空间和初始化。

它的空间分配有三个可能的地方,一是作为类的外部接口的头文件,那里有类声明;二是类定义的内部实现,那里有类的成员函数定义;三是应用程序的 main() 函数前的全局数据声明和定义处。在类外进行初始化,类型 类名:: 变量名 = 初始化值

优势:可以节省内存,因为它是所有对象所公有的,因此,对多个对象来说,静态数据成员只存储一处,供所有对象共用。静态数据成员的值对每个对象都是一样,但它的值是可以更新的。只要对静态数据成员的值更新一次,保证所有对象存取更新后的相同的值,这样可以提高时间效率。

在C++中static的作用

  • (1)在修饰变量的时候,static 修饰的静态局部变量只执行初始化一次,而且延长了局部变量的生命周期,直到程序运行结束以后才释放。
  • (2)static 修饰全局变量的时候,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是 extern 外部声明也不可以。
  • (3)static 修饰一个函数,则这个函数的只能在本文件中调用,不能被其他文件调用。static 修饰的变量存放在全局数据区的静态变量区,包括全局静态变量和局部静态变量,都在全局数据区分配内存。初始化的时候自动初始化为 0。
  • (4)不想被释放的时候,可以使用static修饰。比如修饰函数中存放在栈空间的数组。如果不想让这个数组在函数调用结束释放可以使用 static 修饰。

静态成员的特点:
属于类,不属于某个对象,为所有对象所共享

静态成员变量:在类中,不给静态成员变量分配空间,不能在类中初始化,类中只是声明,需要在类外进行定义和初始化。静态成员变量和静态变量一样, 是在编译期创建并初始化,它在该类的任何对象被建立之前就存在。

静态成员函数:静态函数没有this指针,只能去处理静态的数据

调用静态函数的方式有两种:通过object调用;通过class name 调用

const和staic的区别

const定义的常量在超出其作用域之后其空间会被释放,而static定义的静态常量在函数执行后不会释放其存储空间。

static表示的是静态的。类的静态成员函数、静态成员变量是和类相关的,而不是和类的具体对象相关的。即使没有具体对象,也能调用类的静态成员函数和成员变量。一般类的静态函数几乎就是一个全局函数,只不过它的作用域限于包含它的文件中。

在C++中,static静态成员变量不能在类的内部初始化。在类的内部只是声明,定义必须在类定义体的外部,通常在类的实现文件中初始化,如:double Account::Rate=2.25;static关键字只能用于类定义体内部的声明中,定义时不能标示为static

在C++中,const成员变量也不能在类定义处初始化,只能通过构造函数初始化列表进行,并且必须有构造函数。

const数据成员 只在某个对象生存期内是常量,而对于整个类而言却是可变的。因为类可以创建多个对象,不同的对象其const数据成员的值可以不同。所以不能在类的声明中初始化const数据成员,因为类的对象没被创建时,编译器不知道const数据成员的值是什么。

const数据成员的初始化只能在类的构造函数的初始化列表中进行。要想建立在整个类中都恒定的常量,应该用类中的枚举常量来实现,或者static cosnt。

cosnt成员函数主要目的是防止成员函数修改对象的内容。即const成员函数不能修改成员变量的值,但可以访问成员变量。当方法成员函数时,该函数只能是const成员函数。

static成员函数主要目的是作为类作用域的全局函数。不能访问类的非静态数据成员。类的静态成员函数没有this指针,这导致:1、不能直接存取类的非静态成员变量,调用非静态成员函数2、不能被声明为virtual

inline

1.说明: 函数调用会降低执行效率,增加时间和空间的开销.对于功能简单规模小又使用频繁的函数可设计为内联函数
2.定义: 内联函数不是在调用时发生控制转移,而是在编译时将函数体嵌入在每一个调用处
3.优点: 节省了参数传递,控制转移等开销
4.语法形式: inline 类型说明符 函数名(含类型说明的形参表)
5.注意:

  • 没有用inline修饰的函数也可能被编译成内联函数,对自身直接递归调用的函数肯定无法以内联处理

  • 定义在类内部的成员函数默认定义为内联函数,inline必须和函数定义在一起才能成为真正的inline(仅将inline放在声明前是不起作用的)

  • inline对于编译器只是一个建议,编译器会自动选择是否设置当前函数是否为内联函数

friend

友元函数

允许在类外访问该类的任何成员(包括私有成员)

友元类

整个类可以是另一个类的友元

使用友元类时注意:

(1) 友元关系不能被继承。

(2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。

(3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明

相同class的各个objects互为friend友元

默认成员函数

空类中的默认函数有哪些?
默认的无参构造
析构
拷贝构造
默认的operator=

constructor(ctor,构造函数)

创建对象时,自动调用。

无参构造和全缺省构造都认为是缺省构造函数
(这两个只能用一个,因为调用方法是一样的,否则存在二义性)

overloading(重载) -- ctor构造函数可以有很多个

成员变量的初始化最好放在初值列,这样更快,因为一个变量的数值的设定有两阶段,一个是初始化,一个是后面再赋值

BigThree

假如类有明显地定义下列其中一个成员函数,那么程序员必须连其他二个成员函数也一同编写至类内

拷贝构造

空类中编译器会默认提供一个拷贝构造,默认的拷贝构造函数体代码不为空,参数中的对象成员依次给this 对象成员初始化是一个浅拷贝

创建对象时采用同类对象来初始化,其实是构造函数的重载

拷贝构造传参必须使用引用,如果使用传值,会引发无穷的构造和拷贝构造

拷贝赋值

浅拷贝:
问题:

  1. 多个指针指向同一个空间,在对象回收时,同一块空间会被回收多次,导致程序崩溃。
    2. 修改一个对象指针成员指向的空间里的值,其他对象在引用该值就是修改之后的了
    解决浅拷贝问题:
    1. 避免使用浅拷贝(避免调用拷贝构造)。 尽量 用引用 指针
    2. 深拷贝:手动重构拷贝构造

析构函数

与构造函数功能相反,在对象销毁时,由编译器自动完成,完成一些资源清理和清尾工作

一个类有且只有一个析构函数,若未显示定义,系统会自动生成一个缺省的析构函数

析构函数体内并不是删除对象,而是做一些清理工作,如在malloc,new,new[]之后需要手动释放空间

虚析构

虚析构: 解决了在多态下,回收空间时因未执行子类的析构而导致可能的内存泄露问题。
子类析构:发生了多态行为。
父类析构: 正常回收父类成员
多态条件下,父类的析构一定要虚析构。

多态的缺点:

  1. 内存空间
  2. 效率问题
  3. 安全性问题

构造函数为什么一般不定义为虚函数?而析构函数一般写成虚函数的原因 ?

1、构造函数不能声明为虚函数

1)因为创建一个对象时需要确定对象的类型,而虚函数是在运行时确定其类型的。而在构造一个对象时,由于对象还未创建成功,编译器无法知道对象的实际类型,是类本身还是类的派生类等等

2)虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数即构造函数了
2、析构函数最好声明为虚函数

首先析构函数可以为虚函数,当析构一个指向派生类的基类指针时,最好将基类的析构函数声明为虚函数,否则可以存在内存泄露的问题。

如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除指向派生类的基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。

子类析构时,要调用父类的析构函数吗?

析构函数调用的次序时先派生类后基类的。和构造函数的执行顺序相反。并且析构函数要是virtual的,否则如果用父类的指针指向子类对象的时候,析构函数静态绑定,不会调用子类的析构。

不用显式调用,会自动调用

静态绑定和动态绑定的介绍

静态绑定和动态绑定是C++多态性的一种特性

1)对象的静态类型和动态类型

静态类型:对象在声明时采用的类型,在编译时确定

动态类型:当前对象所指的类型,在运行期决定,对象的动态类型可变,静态类型无法更改

2)静态绑定和动态绑定

静态绑定:绑定的是对象的静态类型,函数依赖于对象的静态类型,在编译期确定

动态绑定:绑定的是对象的动态类型,函数依赖于对象的动态类型,在运行期确定

只有虚函数才使用的是动态绑定,其他的全部是静态绑定

继承&多态

继承

继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。

派生类对象内存按照父类子类属性定义顺序排列

派生类可以访问基类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private。

一个派生类继承了所有的基类方法,但下列情况除外:

  1. 基类的构造函数、析构函数和拷贝构造函数。
  2. 基类的重载运算符。
  3. 基类的友元函数。

继承的三种关系

公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。

保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。

私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。

继承&转换

  • 子类对象可以赋值给父类对象,父类对象不可以赋值给子类对象
  • 父类指针或引用可以指向子类对象;子类则不可以

继承体系中的作用域

子类和父类有同名的成员,子类将屏蔽父类对成员的直接访问,在子类成员函数中,可以使用父类::访问父类成员。这就是隐藏

隐藏:继承关系下,父类和子类中同名的成员之间的关系我们称之为 隐藏
注意:使用父类的成员,我们最好指定父类类名作用域。子类增加新功能,要注意是否将父类的成员隐藏了

单继承:一个子类只有一个直接父类的继承关系

多继承:一个子类有两个或者两个以上父类的继承关系

多态

多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。

父类的指针可以指向任何继承于该类的子类对象,且具有子类对象的行为,多个子类表现为多种形态由父类的指针进行统一管理,当使用基类的指针或引用调用重写的虚函数时,当指向父类调用的就是父类的虚函数,指向子类调用的就是子类的虚函数
父类的指针具有多态性

形成多态必须具备三个条件

1、必须存在继承关系;

2、继承关系必须有同名虚函数(其中虚函数是在基类中使用关键字Virtual声明的函数,在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数);

3、存在基类类型的指针或者引用,通过该指针或引用调用虚函数;

虚函数virtual

虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数,是C++中多态性的一个重要体现。利用基类指针访问派生类中的虚函数,这种情况下采用的是动态绑定技术。

纯虚函数:纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”.纯虚函数不能实例化对象。

non-virtual:不希望derived class派生类重新定义它

virtual虚函数:希望派生类重新定义,但它已有默认的定义

纯虚函数:希望派生类一定要重新定义,没有默认的定义

  • 含有纯虚函数的类叫做抽象类
  • 抽象类不能实例化出对象

注意点

  1. 只有类的成员函数才能定义为虚函数 ( 除了静态的成员函数,构造函数 )
  2. 最好不要把operator=定义为虚函数( 因为使用时容易混滑 !)
  3. 不要在构造函数和析构函数中调用虚函数,因为可能会出现未定义的情况
  4. 最好把基类的析构函数声明为虑函数(因为:虽然基类和派生类的析构函数名不同但是还是会构成重写( 夏盖 ) ------编译器做了特殊处理 )
  5. 基类定义了虚函数,在派生类中该函数仍然保持虚函数属性

当一个类中定义了虚函数,定义对象会在对象内存空间的前面的多申请4个字节空间,存储的是 __vfptr
__vfptr : 二级指针 ,类型为void** ,称为虚函数指针,属于对象,指向了一个数组(虚函数列表)
定于多个对象,存在多个虚函数指针 指向了同一个虚函数列表
在定义对象时,执行构造初始化参数列表中被初始化,指向了虚函数列表

vftable 虚函数列表,本质上是一个数组,每个元素是指向当前类的虚函数的地址
在编译期就存在,属于类的,只有一份

虚函数调用流程:
定义对象 找到 __vfptr -> 虚函数列表,遍历匹配,通过虚函数地址找到虚函数,执行代码.

虚函数列表:

  1. 在继承关系下,父类存在虚函数,子类不但继承了父类的成员属性,也会继承父类的虚函数列表,
  2. 检查子类是否有重写父类的虚函数,如果有,会替换掉父类的虚函数变成子类的虚函数,这个动作 覆盖
  3. 如果没有重写父类虚函数,父类的虚函数会保留,如果子类有自己的单独的虚函数,会顺序添加到虚函数列表中

父类的指针指向子类的对象,子类对象内存空间前4个字节 __vfptr ,
指向了子类的虚函数列表(父类指向哪个子类就是哪个子类的虚函数列表)

抽象类

抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层。

(1)抽象类的定义: 称带有纯虚函数的类为抽象类。

(2)抽象类的作用: 抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。所以派生类实际上刻画了一组子类的操作接口的通用语义,这些语义也传给子类,子类可以具体实现这些语义,也可以再将这些语义传给自己的子类。

(3)使用抽象类时注意:

抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。

抽象类是不能定义对象的。

指针和引用&const

传值&传引用

值传递

单向传递,形参获得了值(拷贝)便与实参脱离关系,形参如何改变都不会影响实参

引用传递&

定义:一种特殊类型的变量,是另一个变量的别名,引用名和变量名效果一样

声明:数据类型 &引用名 = 变量名;

注意:声明引用时,必须同时初始化,指向一个已存在的对象,一旦初始化后,不能改为指向其他对象。

引用传递:引用作为形参,成为实参的一个别名,对形参的操作会直接作用于实参。

传值返回&传引用返回

在使用return 返回时,也内存中也会产生参数的副本,而使用引用结束返回值则避免了这种情况。

引用和指针的区别

  • 引用是变量a的别名,而指针是存储变量a的地址。
  • 引用没有const一说,指针有可以被const修饰。具体指没有int& const a这种形式,而const int& a是有的,前者指引用本身即别名不可以改变,这是当然的,所以不需要这种形式,后者指引用所指的值不可以改变
  • 不存在空引用。引用必须连接到一块合法的内存。引用必须在创建时被初始化。指针可以在任何时间被初始化。
  • 一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。
  • 定义引用不占用额外的内存空间,指针是需要内存空间。
  • 引用的好处:引用作为参数传递时,可以直接给实参进行更改。如果使用指针,则必须先定义一个指针类型,指向要传递的参数,然后再作为实参传递,比较麻烦,且浪费内存空间。

浅拷贝&深拷贝&写时拷贝

浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。

重载

重载overload,覆盖(重写)override,隐藏(重定义)overwrite,这三者之间的区别

1)overload,将语义相近的几个函数用同一个名字表示,但是参数列表(参数的类型,个数,顺序不同)不同,这就是函数重载,返回值类型可以不同

特征:相同范围(同一个类中)、函数名字相同、参数不同、virtual关键字可有可无

2)override,派生类覆盖基类的虚函数,实现接口的重用,返回值类型必须相同

特征:不同范围(基类和派生类)、函数名字相同、参数相同、基类中必须有virtual关键字(必须是虚函数)

3)overwrite,派生类屏蔽了其同名的基类函数,返回值类型可以不同

特征:不同范围(基类和派生类)、函数名字相同、参数不同或者参数相同且无virtual关键字

动态内存管理

new-delete

new-delete malloc-free的区别:

1.new-delete关键字,需要c++编译器支持,malloc-free函数,头文件支持。

2.malloc需要手动指定申请空间的大小(字节数),new指定类型就可以(它根据类型自动计算所需空间大小)。

3.malloc返回的void*需要我们强转,new返回的具体的类型指针,不需要强转。

4.new定义对象会自动调用构造,delete删除对象的时候自动调用析构,malloc-free则不会。

光用malloc/free无法满足动态对象的要求。对象在创建的同时要自动执行函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。因此c++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。

new:先分配memory,再调用ctor

Complex* pc = new Complex(1, 2);

// 编译器转化为:
Complex *pc;
void* mem = operator new (sizeof(Complex)); // 分配内存,operator new 其内部调用malloc(n)
pc = static_cast<Complex*>(mem); // 转型
pc->Complex::Complex(1,2); // 构造函数 

delete:先调用dtor,再释放memory

String* ps = new String("Hello");
delete ps;
// 编译器转化为:
String::~String(ps); // 析构函数
operator delete(ps); // 释放内存,operator delete 内部调用free(ps)

array new 一定要搭配array delete

如果不使用array delete,如果array的元素不是指针,delete过程的析构虽然只调用一次,但是释放内存过程还是会把两个cookie中的内容删掉,但是如果是指针元素,指针指向的内存地址会产生泄露,而cookie本身的内存段不会产生泄露。

设计模式

设计模式:
为了解决某类重复出现的问题而出现的一套行之有效的解决方案。

单例:

  1. 一个类只能定义出一个实例
  2. 能够自主创建这个唯一的实例(而不是使用者去创建)
  3. 能够向整个系统提供这个实例。

单例

饿汉式: 程序一创建就把当前的唯一实例创建出来,生命周期直到程序结束,
所以不用提供销毁的方法,是一种 “空间换时间” 的做法。

class CSington{
public:
	int m_a;
private:
	static CSington m_sin;
private:
	CSington(){  //CSington sin;   //构造私有,在类外不允许定义对象
		m_a = 10;
	}
	CSington(const CSington & sin){  //CSington * pSin3 = new CSington(*CSington::GetSington());
		m_a = sin.m_a;
	}
	~CSington(){  //CSington * pSin2 = CSington::GetSington();
	}
public:
	static CSington* GetSington(){
		return &m_sin;
	}
};
CSington  CSington::m_sin;

懒汉式: 什么时候用到唯一的实例,什么时候创建。提供了销毁的方法,是一种 “时间换空间” 的做法。

class CSington{
public:
	int m_a;
private:
	//static int SingtonCount;
	static CSington * pSin;
private:
	CSington(){
		m_a = 10;
	}
	CSington(const CSington & sin){
		m_a = sin.m_a;
	}
	~CSington(){
		/*if(pSin){
			delete pSin;
			pSin = nullptr;
		}*/
	}
public:
	static CSington* GetSington(){
		if(pSin!=nullptr){
			return pSin;
		}
		//SingtonCount++;
		pSin = new CSington;
		return pSin;
	}
	static void DestroySington(CSington* &pSington){
		if(pSin){
			delete pSin;
			pSin = nullptr;
		}
		pSington = nullptr;
	}
};
CSington * CSington::pSin = nullptr;

模板

函数模板在真正调用时,可以根据形参来自动推到模板参数类型

template :定义模板的关键字 <> 模板参数列表,<>里面指定具体的模板参数类型

#include<iostream>
using namespace std;
#include<typeinfo>
template<typename TT>     //函数模板
TT Add(TT a,TT b){   //模板函数
	TT c = a+b;
	cout<< typeid(c).name()<<endl;  //输出c的类型TT
	return c;
}
template<typename TT,typename KK>     //函数模板
TT Show(TT a,TT b){   //模板函数
	TT c = a+b;
	KK k = 65;  //KK的类型无法推导出来,需要手动指定
	cout<< typeid(c).name()<<endl;
	cout<<k<<"   "<<typeid(k).name()<<endl;
	return c;
}
template<typename TT>     //函数模板
TT Play(TT a,TT b);
int main(){
	Show<long ,char>(a,b);   //如果显式的指定了模板参数类型,最终就是什么类型
	Play<>(a,b);  //可以不指定 推导
	Play<long>(a,b);  //如果实现的时候参数写成long没问题,如果不是long就会报错
	//long __cdecl Play<long>(long,long)
	system("pause");
	return 0;
}
template<typename TT>     //函数模板
TT Play(TT a,TT b){   //模板函数
	TT c = a+b;
	cout<< typeid(c).name()<<endl;
	return c;
}
//char Play(char a,char b){   //模板函数
//	char c = a+b;
//	cout<< typeid(c).name()<<endl;
//	return c;
//}

posted on 2023-01-14 18:08  SocialistYouth  阅读(16)  评论(0编辑  收藏  举报