C++编程

目录

一 C++的概述

1 历史背景

1)C++的江湖地位
java、C、python、C++、c#
2)C++之父:Bjarne Stoustrup(1950–)
–》1979,Cpre,为C语言增加了类的机制
–》1983,C with Class(带类的C),后来C++
–》1985,第一个C++编译器 CFront 1.0
《The C++ Programming Language》
3)C++发展过程
–》1987,GNU C++
–》1990,Borland C++
–》1992,Microsoft C++(VC,VS)

–》1998,ISO C++98
–》2003,ISO C++03
–》2011,ISO C++11
–》2014,ISO C++14
–》2017,ISO C++17
–》*2020,ISO C++20

2 应用领域

1)游戏开发
2)科学计算
3)网络和分布式应用
4)操作系统和设备驱动
5)其它…

3 C和C++

1)都是编译型语言
2)都是强类型语言,但是C++更强
3)C++去除了C中不好的特性
4)C++增加了很多C语言中没有的好的语法特性,全面支持面向对象,比C语言更适合大型软件开发。

二 第一个C++程序

1 编译方式

1)gcc xx.cpp -lstdc++
2)g++ xx.cpp //推荐

2 文件扩展名

1).cpp //推荐
2).cc
3).C
4).cxx

3 头文件

#include <iostream>
–》C++中和I/O相关的类型、对象、函数都在在头文件中
–》C++中大多数头文件没有".h"后缀

注:在C++中开发中,依然可以使用标准C库的头文件,另外在C++中还提供一套不带“.h”替换版本.
#include <stdio.h> ==> #include <cstdio>
#include <stdlib.h> ==> #include <cstdlib>
#include <string.h> ==> #include <cstring>

4 C++中标准输入和输出

1)cin对象表示标准输入//类似scanf
//从键盘读取一个int数据
int num=0;
scanf(“%d”,&num);//C语言
cin >> num;//C++语言
注:“>>”被称为输入(提取)操作符
-------------------------------
//从键盘同时读取一个int和一个double数据
int i=0,double d = 0.0;
scanf(“%d%lf”,&i,&d);//C语言
cin >> i >> d;//C++语言
2)cout对象表示标准输出//类似printf
//打印输出一个int数据
int num = 123;
printf(“%d\n”,num);//C语言
cout << num << endl;//C++语言
注:“<<”被称为输出(插入)操作符
注:“endl”表示换行和"\n"类似
---------------------------------
//同时打印输出一个int数据和一个double
int i=100,double d=1.23;
printf(“%d,%lf\n”,i,d);//C语言
cout << i << ‘,’ << d << endl;//C++语言

三 名字空间(命名空间)

1 名字空间作用

1)避免名字冲突
2)划分逻辑单元

2 定义

namespace 名字空间名{
		名字空间成员1;
		名字空间成员2;
		...
}

注:名字空间成员可以是全局函数、全局变量、类型、名字空间.

3 名字空间成员使用

1)作用域限定操作符"::"
名字空间名::要访问的成员;
eg:
std::cout //指定访问标准名字空间里面的cout
2)名字空间指令
using namespace 名字空间名;
注:在该条指令以后的代码中,指定名字空间的成员都可见,可以直接访问,省略"名字空间名::“.
eg:
using namespace std;
cout << a;//ok
3)名字空间声明
using 名字空间名::名字空间成员;
注:将名字空间中特定的一个成员引入到当前作用域,在该作用域访问这个成员就如同访问自己的局部成员一样,可以直接访问,省略"名字空间名::”

4 全局作用域和匿名名字空间//了解

1)没有放在任何名字空间的成员属于全局作用域,可以直接访问,但如果和局部作用域的成员名字一样,局部优先;这时如果还希望访问全局作用域的成员可以通过“::xx”显式访问。
2)定义名字空间时可以没有名字,即为匿名名字空间,匿名空间中成员和全局作用域类似,可以直接访问,也可以通过"::xx"显式的访问,唯一的区别匿名空间的成员被局限在当前文件中使用。

5 名字空间的嵌套与合并//了解

1)名字空间嵌套

eg:
	namespace ns1{
		int num = 100;
		namespace ns2{
			int num = 200;
			namespace ns3{
				int num = 300;
			}
		}
	} 
	cout << ns1::num << endl;//100
	cout << ns1::ns2::num << endl;//200
	cout << ns1::ns2::ns3::num << endl;//300

2)名字空间合并:两个同名的空间会自动合并成一个
namespace ns{//1.cpp
void func1(void){…}
}
namespace ns{//2.cpp
void func2(void){…}
}

四 C++的结构体、联合体和枚举

1 C++的结构体

1)当定义结构体变量时可以省略struct关键字
2)在C++的结构体内部可以直接定义函数,称为成员函数(方法),在成员函数内部可以直接访问结构体的其它成员.

2 联合体//了解

1)当定义联合体变量时可以省略union关键字
2)在C++中支持匿名联合

3 枚举

1)当定义枚举变量时可以省略enum关键字
2)C++中枚举被看作是独立数据类型,不能把枚举直接当做整型数来使用.

	enum STATE{SLEEP=0,RUN=1,STOP=2};
	/*enum*/ STATE s;
	s = RUN;//C:ok C++:ok
	s = 1;//C:ok C++:error

五 C++的字符串

1 回顾C语言中的字符串

1)字面值常量字符串: “…”
2)字符指针:char*
3)字符数组:char[]

2 C++兼容C风格的字符串,同时增加了string类型,专门字符串

1)定义
string s1;//定义空字符串
string s2 = “xx”;//定义同时初始化
2)字符串拷贝:=
string s1 = “youcw”;
string s2 = “mindasheng”;
s1 = s2;//将s2拷贝给s1
cout << s1 << endl;//“mindasheng”
3)字符串连接:+ +=
string s1 = “hello”;
string s2 = " world";
string s3 = s1 + s2;
cout << s3 << endl;//“hello world”
4)字符串比较:== != > >= < <=
if(s1 == s2){…}
5)随机访问: []
string s = “minwei”;
s[0] = ‘M’;
s[3] = ‘W’;
cout << s << endl;//MinWei
6)暂时可以string理解为是"结构体"类型,里面包含了很多成员函数,常用的:
size()/length();//获取字符串长度
c_str();//返回字符串的起始地址(const char*)
eg:
string str = “youcw”;
str.size();//5
str.length();//5和size()等价
------------------
string s1 = “hello”;//C++风格
const char* s2 = “world”;//C风格

s1 = s2;//ok
s2 = s1;//error
s2 = s1.c_str();

练习:使用string表示字符串,从键盘读取一个字符串并统计里面包含字符"Y/y"个数。
提示:
string str;
cin >> str;//从键盘读取一个字符串

练习:使用string表示字符串,从键盘读取一个字符串,实现字符串反转。
提示:
输入:abcdef
输出:fedcba

六 C++的布尔类型(bool)

1 bool类型是C++中的基本数据类型,专门表示逻辑值,逻辑真用true表示,逻辑假false表示
2 bool类型在内存占一个字节:1表示true,0表示false
3 bool类型变量可以接收任意类型表达式结果,其值非0则为true,为0则为假。

七 操作符别名//了解

&& <==> and
|| <==> or
{  <==> <%
}  <==> %>
...

八 C++的函数

1 函数重载(overload)

1)定义
在相同作用域,可以定义同名的函数,但是参数必须有所区分,这样函数构成重载关系.
注:函数重载和返回类型无关。

eg:实现图形库中一些绘图函数
//C语言
void drawRect(int x,int y,int w,int h){}
void drawCircle(int x,int y,int r){}

-----------------
//C++语言
void draw(int x,int y,int w,int h){}
void draw(int x,int y,int r){}

2)函数重载匹配
调用重载关系的函数时,编译器将根据实参和形参的匹配程度,自动选择最优的重载版本,当前g++编译器匹配一般规则:
完全匹配>=常量转换>升级转换>降级转换>省略号

3)函数重载原理
C++的编译器在编译函数时,会进行换名,将参数表的类型信息整合到新的函数名中,因为重载关系的函数参数表有所区分,换出的新的函数名也一定有所区分,解决了函数重载和名字冲突的矛盾。

笔试题:C++中extern "C"作用?
在C++函数声明时加extern "C",要求C++编译器不对该函数进行换名,便于C程序直接调用该函数.

注:extern "C"的函数无法重载。

2 函数的缺省参数(默认实参)

1)可以为函数参数指定缺省值,调用该函数时,如果不给实参,就取缺省值作为默认实参。
void func(int i,int j=0/缺省参数/){}
2)靠右原则:如果函数的某个参数带有缺省值,那么该参数右侧的所有参数都必须带有缺省值。
3)如果函数的声明和定义分开写,缺省参数应该写在函数的声明部分,而定义部分不写。

3 函数的哑元参数

1)定义函数时,只有类型而没有变量名的形参被称为哑元
void func(int){…}
2)需要使用哑元场景
–》在操作符重载函数中,区分前后++、-- //后面讲
–》兼容旧的代码

算法库:void math_func(int a,int b){...}
使用者:
	int main(void){
		...
		math_func(10,20);
		...
		math_func(10,20);
		...
	}
-----------------------------------------
升级算法库:void math_func(int a,int/*哑元*/){...}
使用者:
	int main(void){
		...
		math_func(10,20);
		...
		math_func(10,20);
		...
	}

4 内联函数(inline)

1)使用inilne关键字修饰的函数,即为内联函数,编译器将会尝试进行内联优化,可以避免函数调用开销,提高代码执行效率.
inline void func(void){…}
2)使用说明
–》多次调用小而简单的函数适合内联优化
–》调用次数极少或大而复杂的函数不适合内联
–》递归函数不能内联优化
–》虚函数不能内联优化//后面讲

注:内联只是一种建议而不是强制的语法要求,一个函数能否内联优化主要取决于编译器,有些函数不加inline修饰也会默认处理为内联优化,有些函数即便加了inline修饰也会被编译器忽略。

九 C++动态内存管理

1 回顾C语言动态内存管理
1)分配:malloc()
2)释放:free()

2 C++动态内存管理
1)分配:new/new[]
2)释放:delete/delete[]

十 C++引用(Reference)

1 定义

1)引用即别名,引用就是某个变量别名,对引用操作和对变量本身完全相同.
2)语法
类型 & 引用名 = 变量名;
注:引用必须在定义同时初始化,而且在初始化以后所绑定的目标变量不能再做修改.
注:引用类型和绑定目标变量类型要一致。

2 常引用

1)定义引用时可以加const修饰,即为常引用,不能通过常引用修改目标变量.
const 类型 & 引用名 = 变量名;
类型 const & 引用名 = 变量名;//和上面等价

2)普通引用也可以称为左值引用,只能引用左值;而常引用也可以称为万能引用,既可以引用左值也可以引用右值。

3)关于左值和右值
左值(lvalue):可以放在赋值表达式左侧,可以被修改
右值(rvalue):只能放在赋值表达式右侧,不能被修改

练习:测试下面表达式结果,是左值还是右值?
int a,b;
a+b;//右值
a+=b;//左值
++a;//左值
a++;//右值

3 引用型函数参数

1)可以将引用用于函数的参数,这时形参就是实参别名,可以通过形参直接修改实参的值;同时还能避免函数调用时传参的开销,提高代码执行效率。
2)引用型参数有可能意外修改实参的值,如果不希望修改实参,可以将形参声明为常引用,提高效率的同时还可以接收常量型的实参。

4 引用型函数返回值

1)可以将函数的返回类型声明为引用,这时函数的返回结果就是return后面数据的别名,可以避免返回值带来的开销,提高代码执行效率.
2)如果函数返回类型是左值引用,那么函数调用表达式结果就也将是一个左值。
注:不要从函数中返回局部变量的引用,因为所引用的内存会在函数返回以后被释放,使用非常危险!可以在函数中返回成员变量、静态变量、全局变量的引用。

int& func(void){
	...
	return num;
}
func() = 100;//ok

5 引用和指针

1)如果从C语言角度看待引用的本质,可以认为引用就是通过指针实现的,但是在C++开发中,推荐使用引用,而不推荐使用指针.
int i = 100;
int* const pi = &i;
int& ri = i;
pi <=等价=> ri
2)指针可以不做初始化,其目标可以在初始化以后随意改变(指针常量除外),而引用必须做初始化,而且一旦初始化所引用的目标不能再改变.
int a=10,b=20;
int
p;//ok
p = &a;
p = &b;
--------------------
int& r;//error
int& r = a;
r = b;//ok,但不是修改引用目标,仅是赋值运算

//下面内容了解
3)可以定义指针的指针(二级指针),但是不能定义引用的指针.
int a = 10;
int* p = &a;
int** pp = &p;//二级指针
------------------------
int& r = a;
int&* pr = &r;//error,引用的指针
int* pr = &r;//ok,仅是普通指针

4)可以指针的引用(指针变量别名),但是不能定义引用的引用。
int a = 100;
int* p = &a;
int* & rp = p;//ok,指针的引用
-----------------------------
int& r = a;
int& & rr = r;//error,引用用的引用
int& rr = r;//ok,但是仅是一个普通引用

5)可以指针数组,但是不能定义引用数组
int i=10,j=20,k=30;
int* parr[3] = {&i,&j,&k};//ok,指针数组
int& rarr[3] = {i,j,k};//error

6)可以定义数组引用(数组别名)
int i=10,j=20,k=30;
int arr[3] = {i,j,k};
int (&rarr)[3] = arr;//ok, 数组引用

7)和函数指针类似,也可以定义函数引用(函数别名)
void func(int i){}
int main(void){
void (*pf)(int) = func;//函数指针
void (&rf)(int) = func;//函数引用
pf(100);
rf(100);
}

十一 类型转换

1 隐式类型转换

char c = ‘A’;
int i = c;//隐式
void func(int i){}
func©;//隐式
int func(void){
char c=‘A’;
return c;//隐式
}

2 显示类型转换

2.1 C++兼容C中强制类型转换
char c = ‘A’;
int i = (int)c;//C风格
int i = int©;//C++风格

2.2 C++扩展了四种操作符形式显式转换
1)静态类型转换:static_cast
语法:
目标变量 = static_cast<目标类型>(源类型变量);
适用场景:
主要用于将void*转化为其它类型的指针

2)动态类型转换:dynamic_cast//后面讲
语法:
目标变量 = dynamic_cast<目标类型>(源类型变量);

3)去常类型转换:const_cast
语法:
目标变量 = const_cast<目标类型>(源类型变量);
适用场景:
主要用于去掉指针或引用const属性.

4)重解释类型转换:reinterpret_cast
语法:
目标变量=reinterpret_cast<目标类型>(源类型变量);
适用场景:
在指针和整型数进行显式转换.
任意类型指针或引用之间显式转换.

eg:已知物理内存地址0x12345678,向该地址存放一个整型数100?
int* paddr = reinterpret_cast<int*>(0x12345678);
*paddr = 100;

小结:
1 慎用宏,可以使用const、enum、inline替换
#define PAI 3.14
–》const double PAI = 3.14;

#define SLEEP 0
#define RUN 1
#define STOP 2
–》enum STATE{SLEEP,RUN,STOP};

#define Max(a,b) ((a)>(b)?(a):(b))
–》inline int Max(int a,int b){
return a > b ? a : b;
}

2 变量随用随声明同时初始化
3 尽量使用new/delete替换malloc/free
4 少用void*,指针计算,联合体和强制转换
5 尽量使用string表示字符串,少用C风格的char*/char[]

十二 类和对象//了解

1 什么是对象
万物皆对象,任何一种事物都可以看做是对象.

2 如何描述对象
通过对象的属性和行为来描述对象.

3 面向对象程序设计
对自然世界中对象观察和描述引入到编程中一种理念和方法,这种方法称为"数据抽象",即在描述对象时把细节东西玻璃出去,只考虑一般性的、有规律性的、统一性的东西.

4 什么是类
类就是将多个对象共性提取出来定义的一种新的数据类型,是对 对象 属性和行为的抽象描述.

现实世界 类 虚拟世界
具体对象–抽象–>属性/行为–实例化–>具体对象

十三 类的定义和实例化

1 类定义的一般语法形式

struct/class 类名:继承方式 基类,...{
访问控制限定符:
		类名(形参表):初始化列表{}//构造函数
		~类名(void){}//析构函数
		返回类型 函数名(形参表){}//成员函数
		数据类型 变量名;//成员变量
};

2 访问控制限定符

1)public:公有成员,任何位置都可以访问。struct定义的类默认是公有
2)private:私有成员,只有类自己的成员函数才能访问。class定义的类默认是私有
3)protected:保护成员

3 构造函数(constructor)

1)语法
class 类名{
类名(参数表){
主要负责初始化对象,即初始化成员变量。
}
};
2)函数名和类名一致,没有返回类型。
3)构造函数在创建对象时自动被调用,不能像普通的成员函数一样显式的调用.
4)在每个对象的生命周期,构造函数一定会被调用,且仅会被调用一次。

练习:实现一个电子时钟类,使用构造函数初始化时钟的时间为当前的系统时间,并可以以秒为单位运行
提示:
class Clock{
public:
构造函数(time_t t){
tm* local = localtime(&t);
时 = local->tm_hour;
分 = local->tm_min;
秒 = local->tm_sec;
}
void run(void){
while(1){打印当前时间;计时+1秒;sleep(1);}
}
private:
int 时,分,秒;
};
Clock c( time(NULL) );
c.run();

4 对象的创建和销毁

1)在栈区创建单个对象 //重点掌握
类名 对象(构造实参表);//直接初始化
类名 对象=类名(构造实参表);//拷贝初始化(实际等价)
eg:
string s;
string s(“hello”);
string s = string(“hello”);//string s = “hello”;

2)在栈区创建多个对象(对象数组)
类名 对象数组[元素个数] = {
类名(构造实参表),类名(构造实参表),…};

3)在堆区创建/销毁单个对象 //重点掌握
创建:
类名* 对象指针 = new 类名(构造实参表);
注:new操作符会先分配内存再自动调用构造函数,完成对象的创建和初始化;而如果是malloc函数只能分配内存,不会调用构造函数,不具备创建对象能力.

销毁:
delete 对象指针;

4)在堆区创建/销毁多个对象
创建:
类名* 对象指针 = new 类名[元素个数] {
类名(构造实参表),类名(构造实参表),…};
销毁:
delete[] 对象指针;

5 多文件编程:类的声明和定义可以分别放在不同的文件中

1)类的声明一般放在头文件中(xx.h)
2)类的实现一般放在源文件中(xx.cpp)

十四 构造函数和初始化列表

1 构造函数可以重载,也可以带有缺省参数

//匹配string的无参构造函数
string s;
//匹配string的有参(const char*)构造函数      
string s("hello");
---------------------------------
http://www.cplusplus.com/

2 缺省构造函数(无参构造函数)

1)如果类中没有定义任何构造函数,编译器会为该类提供一个缺省(无参)构造函数:
–》对于基本类型成员变量不做初始化
–》对于类 类型的成员变量(成员子对象),将会自动调用相应类的无参构造函数来初始化

2)如果自己定义了构造函数,无论是否有参数,那么编译器都不会再提供缺省的无参构造函数了.

3 类型转换构造函数(单参构造函数)

class 类名{
	//可以将源类型变量转换为当前类类型对象.
	类名(源类型){...}
};
-----------------------------------
class 类名{
	//加“explicit”关键字修饰,可以强制要求这种类型
	//转换必须显式的完成.
	explicit 类名(源类型){...}
};

4 拷贝构造函数(复制构造函数)

1)用一个已存在的对象作为同类对象的构造实参,创建新的副本对象时,会调用该类拷贝构造函数。
class 类名{
类名(const 类名&){//拷贝构造

}
};
------------
eg:
class A{…};
A a1(…);
A a2(a1);//匹配A的拷贝构造函数

2)如果一个类没有自己定义拷贝构造函数,那么编译器会为该类提供一个缺省的拷贝构造函数:
–》对于基本类型的成员变量,按字节复制
–》对于类类型的成员变量(成员子对象),将自动调用相应类的拷贝构造函数来初始化

注:一般不需要自己定义拷贝构造函数函数,因为编译器缺省提供的已经很好用了.

class A1{};//缺省无参,缺省拷贝
class A2{//缺省拷贝
	A(void){}
};
class A3{//缺省拷贝
	A(int){}
};
class A4{//没有缺省构造
	A(const A&){}
};

3)拷贝构造函数调用时机
–》用一个已存在对象作为同类对象的构造实参
–》以对象形式向函数传递参数
–》从函数中返回对象(有可能被编译器优化掉)

5 初始化列表

1)语法
class 类名{
类名(参数表):成员变量1(初值),成员变量2(初值){

}
};
eg:
class Student{
public:
//先定义成员变量,再赋初值
Student(const string& name,int age,int no){
m_name = name;
m_age = age;
m_no = no;
}
//定义成员变量同时初始化
Student(const string& name,int age,int no)
:m_name(name),m_age(age),m_no(no){
}
private:
string m_name;
int m_age;
int m_no;
};
2)大多数情况使用初始化列表和在构造函数体赋初值没有太大区别,两种形式可以任选;但是有以下特殊场景必须要使用初始化列表:
–》如果有类 类型的成员变量(成员子对象),并希望以有参方式对其进行初始化,则必须使用初始化列表显式指明成员子对象需要的构造实参。
–》如果类中包含"const"或"引用"成员变量,则必须在初始化列表显式的初始化。
注:成员变量的初始化顺序由声明顺序决定,而与初始化列表的顺序无关,所以不要使用一个成员变量去初始化另一个成员变量.

练习:使用初始化列表为时钟类增加计时器功能,如果使用系统时间构造对象,则表现为时钟功能;如果使用无参方式构造对象,则使用初始化列表将时间初始化为"00:00:00",则表现为计时器功能.

十五 this指针和常成员函数

1 this指针

1)类中的成员函数(包括构造函数、析构函数)都有一个隐藏的当前类类类型的指针参数,名为this。在成员函数中访问类中其它成员,其本质都是通过this来实现的。
2)对于普通的成员函数,this指针就是指向该成员函数的调用对象;对于构造函数,this指针就指向正在创建的对象。
3)大多数情况,可以忽略this直接访问类中的成员,但是在以下几个特殊场景必须要显式使用this指针:
–》区分作用域
–》从成员函数返回调用对象自身(返回自引用)//重点掌握
–》从类的内部销毁对象自身(对象自销毁) //了解
–》作为成员函数实参,实现对象之间交互 //了解

2 常成员函数(常函数)

1)在一个成员函数参数表后面加上const,这个成员函数就是常成员函数。
返回类型 函数名(参数表) const {函数体}
2)常成员函数中的this指针是一个常量指针,不能在常成员函数中修改成员变量的值。
3)被mutable关键字修饰的成员变量,可以在常成员函数中被修改。
4)非常对象既可以调用常函数也可以调用非常函数;而常对象只能调用常函数,不能调用非常函数.
注:常对象也包括常指针和常引用
5)同一个类中,函数名形参表相同的成员函数,其常版本和非常版本可以构成有效的重载关系,常对象匹配常版本,非常对象匹配非常版本.

十六 析构函数(Destructor)

1 语法
class 类名{
~类名(void){
//主要负责清理对象生命周期中的动态资源
}
};
1)函数名必须是"~类名"
2)没有返回类型,也没有参数
3)不能被重载,一个类只能有一个析构函数、
2 当对象被销毁时,相应类的析构函数将被自动执行
1)栈对象当其离开所在作用域时,其析构函数被作用域终止“右化括号”调用
2)堆对象的析构函数被delete操作符调用

3 如果一个类自己没有定义析构函数,那么编译器会为该类提供一个缺省的析构函数:
1)对基本类型的成员变量,什么也不做
2)对类 类型的成员(成员子对象),将会自动调用相应类的析构函数.

4 对象创建和销毁的过程
1)创建
–》分配内存
–》构造成员子对象(按声明顺序)
–》执行构造函数代码
2)销毁
–》执行析构函数代码
–》析构成员子对象(按声明逆序)
–》释放内存

十七 拷贝构造和拷贝赋值

1 浅拷贝和深拷贝 //参考copy.png
1)如果一个类中包含了指针形式的成员变量,缺省的拷贝构造函数只是赋值了指针变量自身,而没有复制指针所指向的内容,这种拷贝方式被称为浅拷贝.
2)浅拷贝将会导致不同对象之间的数据共享,如果数据在堆区,析构时还可能会出现"double free"的异常,为此就必须自己定义一个支持复制指针所指向内容的拷贝构造函数,即深拷贝。

2 拷贝赋值 //参考copy2.png
1)当两个对象进行赋值运算时,比如"i3=i2",编译器会将其处理为“i3.operator=(i2)”成员函数调用形式,其中“operator=”被称为拷贝赋值函数,由该函数完成两个对象的赋值运算,该函数的返回结果就是赋值表达式结果.
2)如果自己没有定义拷贝赋值函数,那么编译器会提供一个缺省的拷贝赋值函数,但是缺省的拷贝赋值和缺省拷贝构造类似,也是浅拷贝,只是赋值指针变量本身,而没有复制指针所指向的内容,有数据共享、double free、内存泄漏的问题。
3)为了避免浅拷贝的问题,必须自己定义深拷贝赋值函数:
类名& operator=(const 类名& that){
if(&that != this){//防止自赋值
释放旧内存;
分配新内存;
拷贝新数据;
}
return *this;//返回自引用
}

练习:实现String类,提示
class String{
public:
//构造函数
String(const char* str=NULL){}
//析构函数
~String(void){}
//拷贝构造
String(const String& that){}
//拷贝赋值
String& operator=(const String& that){}
private:
char* m_str;
};

十八 静态成员(static)

1 静态成员变量

1)语法
class 类名{
static 数据类型 变量名;//声明
};
数据类型 类名::变量名 = 初值;//定义和初始化
2)普通成员变量属于对象,而静态成员变量不属于对象,静态变量内存在全局区,可以把静态变量理解为被限制在类中使用的全局变量.
3)普通成员变量在对象构造时定义和初始化,而静态成员变量在类的外部单独定义和初始化。
4)使用方法
类名::静态成员变量;//推荐
对象.静态成员变量;//和上面等价

2 静态成员函数

1)语法
class 类名{
static 返回类型 函数名(参数表){…}
};
2)静态成员中没有this指针,也没有const属性,可以把静态成员函数理解为被限制在类作用域使用的全局函数.
3)使用方法
类名::静态成员函数(实参表);//推荐
对象.静态成员函数(实参表);//和上面等价
注:在静态成员函数中只能访问静态成员,不能访问非静态成员;在非静态成员函数既可以访问静态成员,也可以访问非静态成员.

3 单例模式

1)概念
一个类只允许存在唯一的对象,并提供它的方法.
2)实现思路
–》禁止在类的外部创建对象:私有化构造函数即可
–》类的内部维护唯一的对象:静态成员变量
–》提供单例对象的访问方法:静态成员函数
3)具体创建方式
–》饿汉式:单例对象无论用或不用,程序启动即创建
–》懒汉式:单例对象用时再创建,不用即销毁

十九 成员指针//了解

1 成员变量指针

1)定义
类型 类名::*成员指针变量名 = &类名::成员变量;
2)使用
对象.成员指针变量名;
对象指针->成员指针变量名;
注:“.
”和“->
”是一个符号,不能写分家了

2 成员函数指针

1)定义
返回类型 (类名::*成员函数指针)(参数表)
= &类名::成员函数名;
2)使用
(对象.*成员函数指针)(实参表);
(对象指针->*成员指针变量名)(实参表);

二十 操作符重载(operator)

1 双目操作符重载 L#R

1.1 计算类双目操作符: + - …
1)表达式结果是右值,不能对表达式结果再赋值
2)左右操作数既可以是左值也可以是右值
3)两种具体实现方式
–》成员函数形式(左调右参)
L#R的表达式可以被编译器处理为"L.operator#®"成员函数调用形式,该函数的返回就是表达式结果。
–》全局函数形式(左右都参)
L#R的表达式可以被编译器处理为"operator#(L,R)"全局函数调用形式,该函数的返回就是表达式结果。
注:通过friend关键字可以把一个全局函数声明为某个类的友元,对于友元函数可以访问类中的任何成员。

1.2 赋值类双目操作符: += -= …
1)表达式结果是左值,就是左操作数的自身
2)左操作数一定是左值,右操作数可以是左值也可以是右值
3)两种具体实现方式
–》成员函数形式(左调右参),L#R ==> L.operator#®
–》全局函数形式(左右都参),L#R ==> operator#(L,R)

2 单目操作符重载 #O

1.1 计算类单目操作符:-(负) ~(位反) …
1)表达式结果是右值,不能对表达式结果再赋值
2)操作数既可以是左值也可以是右值
3)两种具体实现方式
–》成员函数形式:#O ==> O.operator#()
–》全局函数形式:#O ==> operator#(O)

1.2 自增减单目操作符:++ – …
1)前++、–
–》表达式结果是左值,就是操作数自身
–》操作数一定是左值
–》两种具体实现方式
成员函数形式:#O ==> O.operator#()
全局函数形式:#O ==> operator#(O)

2)后++、–
–》表达式结果是右值,是操作数自增减前副本,不能对表达结果再赋值
–》操作数一定是左值
–》两种具体实现方式
成员函数形式:O# ==> O.operator#(int/哑元/)
全局函数形式:O# ==> operator#(O,int/哑元/)

3 输出(插入)和输入(提取)操作符重载: << >>

功能:实现自定义类型对象的直接输出或输入
注:只能使用全局函数形式,不能使用成员函数形式

#include <iostream>
ostream //标准输出流类,cout就是该类实例化的对象
istream //标准输入流类,cin就是该类实例化的对象

//全局函数形式:operator<<(cout,a)
cout << a;
//全局函数形式:operator>>(cin,a)
cin << b;
------------------------------------------------
friend ostream& operator<<(ostream& os,const Right& right){
	...
	return os;
}
friend istream& operator>>(istream& is,Right& right){
	...
	return is;
}

练习:实现3*3矩阵类,支持下面操作符重载
+ - += -= -(负) 前++、-- 后++、-- <<(输出)
* *=
提示:
1 2 3 9 8 7 10 10 10
4 5 6 + 6 5 4 = 10 10 10
7 8 9 3 2 1 10 10 10
--------------------------------
1 2 3 9 8 7 -8 -6 -4
4 5 6 - 6 5 4 = 2 0 2
7 8 9 3 2 1 4 6 8
--------------------------------
1 2 3 9 8 7 30 24 18
4 5 6 * 6 5 4 = 84 69 54
7 8 9 3 2 1 128 114 90

	A 1~2
	B 3~4
	C 5~6
	D 其它

4 下标操作符重载 []

功能:实现自定义类型对象能够像数组一样去使用
注:非常对象返回左值,常对象返回右值
string s = “minwei”;
s[0] = ‘M’;//s.operator = ‘M’
s[3] = ‘W’;//s.operator = ‘W’
cout << s << endl;//“MinWei”
---------------------------------
const string s2 = “youchengwei”;
cout << s2[0] << endl;//y
s2[0] = ‘Y’;//error

5 函数操作符重载 ()

功能:实现让自定义类型对象像函数一样使用//仿函数
注:对于参数个数、参数类型和返回类型没有任何限制
A a(…);
a(100,1.23);//a.operator()(100,1.23)

6 new/delete操作符重载 //了解

static void* operator new(size_t size){…}
static void operator delete(void* p){…}

7 操作符重载限制

1)不是所有操作符都能重载,下面几个操作符不能重载
–》作用域限定操作符 “::”
–》直接成员访问操作符 “.”
–》直接成员指针解引用操作符 “.*”
–》条件操作符 “?:”
–》字节长度操作符 “sizeof”
–》类型信息操作符 “typeid”//后面讲
2)如果一个操作符所有的操作数都是基本类型,则无法重载
3)操作符重载不会改变预定的优先级
4)操作符重载不会改变操作数个数
5)不能通过操作符重载发明新的操作符
6)只能使用成员函数形式重载的操作符
= [] () ->

二十一 继承(Inheritance)

1 继承的概念 //了解

通过一种机制描述类型之间共性和特性的方式,利用已有的数据类型定义新的数据类型,这种机制就是继承.
eg:
	人  类:姓名、年龄、吃饭、睡觉
	学生类:姓名、年龄、吃饭、睡觉、学号、学习
	教师类:姓名、年龄、吃饭、睡觉、工资、讲课
	...
	------------------------------------------
	人  类:姓名、年龄、吃饭、睡觉
	学生类 继承 人类:学号、学习
	教师类 继承 人类:工资、讲课
	...
	
	人类(基类/父类)
   /    \

学生类 教师类(派生类/子类)

基类–派生–>子类
子类–继承–>基类

2 继承的语法

class 子类:继承方式 基类1,继承方式 基类2,...{
	......
};
继承方式:
--> 公有继承(public)
--> 保护继承(protected)
--> 私有继承(private)

3 公有继承(public)的语法特性

1)子类对象会继承基类的属性和行为,通过子类对象可以访问基类中的成员,如同是基类对象在访问它们一样。
注:子类对象中包含的基类部分被称为“基类子对象”
2)向上造型(upcast)//重点掌握
将子类类型的指针或引用转换为基类类型的指针或引用;这种操作性缩小的类型转换,在编译器看来是安全的,可以直接隐式转换.
基 类

子 类
eg: class A{};
class B:public A{};
class C:public A{};
class D:public A{};

void func1(A* pa){}
void func2(A& ra){}
int main(void){
func1(&B/&C/&D…);//向上造型
func2(B/C/D…);//向上造型
}
3)向下造型(downcast)
将基类类型的指针或引用转换为子类类型的指针或引用;这种操作性放大的类型转换,在编译器看来是危险的,不能隐式转换,但可以显式转换(推荐static_cast)。
基 类

子 类

4)子类继承基类的成员
–》在子类中,可以直接访问基类中的公有和保护成员,就如同是子类自己的成员一样。
–》基类中的私有成员,子类也可以继承,但是受到访问控制属性的影响,无法直接访问;如果希望访问基类中的私有成员,可以让基类提供公有或保护的成员函数,来间接访问。

5)子类隐藏基类的成员
–》如果子类和基类定义了同名的成员函数,因为所属作用域不同,不能构成有效的重载关系,而是一种隐藏关系;通过子类对象将会优先访问子类自己的成员,基类的成员无法被使用。
–》如果希望访问基类中被隐藏的同名成员,可以通过"类名::"显式指明 //推荐
–》如果同名的成员函数参数不同,也可以通过using声明,将基类中的成员函数引入子类的作用域中,让它们形成重载,通过函数重载的参数匹配来解决。

4 访问控制属性和继承方式
1)访问控制属性:影响类中成员的访问位置
访问控制限定符 访问控制属性 内部访问 子类访问 外部访问 友元访问
public 公有成员 ok ok ok ok
protected 保护成员 ok ok no ok
private 私有成员 ok no no ok

2)继承方式:影响通过子类访问基类中成员的可访问性
基类中的成员 公有继承的子类 保护继承的子类 私有继承的子类
公有成员 公有成员 保护成员 私有成员
保护成员 保护成员 保护成员 私有成员
私有成员 私有成员 私有成员 私有成员

eg:
class _Base{
public:
void func(void){…}
};
class Base:private _Base{//ok
};
class A:public Base{};//no
class B:public A{};//no
class C:public B{};//no

--------------------------------
eg:
class Base{
public:
void func(void){…}
};
//class Derived:public Base{};
class Derived:private Base{};
int main(void){
Derived d;
Base* pb = &d;//向上造型,error
pb->func();
}
注:向上造型语法特性在保护继承和私有继承不再适用。

5 子类的构造函数//重点掌握

1)如果子类构造函数没有显式指明基类子对象的初始化方式,那么编译器将会自动调用基类的无参构造函数来初始化基类子对象。
2)如果希望基类子对象以有参的方式被初始化,则需要在子类构造函数的初始化列表中指明基类子对象的初始化方式。

二十二 多态(polymorphic)

1 虚函数覆盖(函数重写)、多态概念

1)如果基类中某个成员函数被声明为虚函数,那么子类中和该函数具有相同的成员函数就也是虚函数,并且对基类中版本形成覆盖,即函数重写。
2)满足虚函数覆盖关系后,这时通过指向子类对象的基类指针或者通过引用子类对象的基类引用,去调用虚函数,实际被执行的将是子类中的覆盖版本,而不是基类中的原始版本,这种语法现象被称为多态。
class Base{
public:
virtual void func(void){}//虚函数
};
class Derived:pubilc Base{
void func(void){}//也是虚函数
}
Derived d;
Base* pb = &d;//pb指向子类对象的基类指针
Base& rb = d;//rb引用子类对象的基类引用
pb->func();//实际被执行的将是子类中的覆盖版本
rb.func();//实际被执行的将是子类中的覆盖版本

2 虚函数覆盖(函数重载)条件

1)只有类中成员函数才能被声明为虚函数,而全局函数、静态成员函数、构造函数都不能声明为虚函数。
注:析构函数可以为虚函数(特殊,后面讲)
2)只有在基类中以virtual关键字修饰的成员函数才能作为虚函数被子类中版本覆盖,而与子类中虚函数是否加virtual关键字无关。
3)虚函数在基类中的原始版本和子类中的覆盖版本必须具有相同的函数签名,即函数名、参数表和常属性一致。
4)如果基类中的虚函数返回基本类型的数据,那么该函数在子类中覆盖版本必须返回相同类型的数据;而如果基类中的虚函数返回类类型的指针(A*)或引用(A&),那么允许子类中的覆盖版本返回其子类类型的指针(B*)或引用(B&)。
class A{};
class B:public A{};

3 多态的条件

1)多态的语法现象除了满足虚函数覆盖,还必须通过指针或引用调用虚函数才能表现出来。
2)调用虚函数的指针也可以是this指针,如果通过子类对象调用基类中的成员函数,在该成员函数中的this指针将是一个指向子类对象的基类指针,再使用this去调用虚函数,也可以表现多态的语法现象。//重点掌握
eg:Qt中的多线程
class QThread{//线程类,官方写好的类
public:
void start(void){//开启线程
this->run();
}
protected:
virtual void run(void){//线程入口函数
}
};
class MyThread:public QThread{
protected:
void run(void){//重写线程入口函数
//需要放在线程执行的代码
}
};
MyThread thread;
thread.start();//开启子线程,重写的run函数将在子线程中被执行

4 纯虚函数、抽象类和纯抽象类

1)纯虚函数
virtual 返回类型 函数名(形参表)[const] = 0;
2)抽象类
如果类中包含了纯虚函数,那么该类就是抽象类。
注:抽象类不能创建对象.
3)纯抽象类(接口、接口类)
如果类中所有的成员函数都是纯虚函数,那么该类就是纯抽象类。

5 多态原理//了解

通过虚函数表和动态绑定实现了多态的语法//参考polymorphic.png
1)虚函数表会增加内存的开销
2)动态绑定有时间的开销
3)虚函数不能被内联优化
总结:实际开发中如果没有多态的语法要求,最好不要使用虚函数。

6 虚析构函数

问题:基类析构函数不会调用子类的析构函数,如果对一个指向子类对象的基类指针使用delete运算,实际被执行的将是基类的析构函数,子类的析构函数不会被执行,有内存泄漏风险

解决:如果将基类中的析构函数声明为虚函数,那么子类中的析构函数也就是一个虚析构函数,并且可以对基类中版本形成覆盖,可以表现多态的语法;这时如果对一个指向子类对象的基类指针使用delete运算符,实际被执行的将是子类的析构函数,子类的析构函数在执行结束后又会自动调用基类的析构函数,从而避免内存泄漏。

二十三 运行时的类型信息//了解

1 typeid操作符
#include
typeid(类型/对象);//返回typeinfo对象,用于描述类型信息

2 dynamic_cast操作符
语法:
目标变量 = dynamic_cast<目标类型>(源类型变量);
适用场景:
主要用于具有多态特性父子类指针或引用之间的显式类型转换.

二十四 C++异常机制(exception)

1 软件开发中的常见错误
1)语法错误
2)逻辑错误
3)功能错误
4)设计缺陷
5)需求不符
6)环境异常
7)操作不当

2 传统C语言中的错误处理
1)通过返回值表示错误
优点:函数调用路径中的所有栈对象可以得到正确析构,内存管理安全。
缺点:错误处理流程比较麻烦,需要逐层进行返回判断,代码臃肿。

2)通过远程跳转处理错误
优点:错误处理流程比较简单,不需要逐层的返回值判断,一步到位的错误处理,代码精炼
缺点:函数调用路径中的栈对象失去了被析构的机制,有内存泄漏的风险

3 C++异常语法
1)异常抛出
throw 异常对象;
注:异常对象可以是基本类型的数据,也可以是类类型对象.

2)异常检测和捕获
try{
可能引发异常的语句;
}
catch(异常类型1){
针对异常类型1数据的处理.
}
catch(异常类型2){
针对异常类型2数据的处理.
}

注:catch子句根据异常对象的类型自上而下顺序匹配,而不是最优匹配,因此对子类异常捕获语句要写在前面,否则会被基类异常捕获语句提前截获。

4 函数异常说明
1)语法
返回类型 函数名(参数表) throw(异常类型表) {}
注:“throw(异常类型表)”即为异常说明表,用于说明该函数可能抛出的异常类型
2)函数异常说明只是一种承诺,不是强制语法要求,如果函数抛出了异常说明以外的其它类型,则无法被函数的调用者正常检测和捕获,而会被系统捕获,导致进程终止。

3)函数异常说明的两种极端形式
–》不写函数异常说明,表示可以抛出任何异常。
–》空异常说明,“throw()”,表示不可以抛出任何异常。

4)如果函数的声明和函数定义分开,要保证异常说明表中的类型一致,但是顺序无所谓。

5 标准异常类exception
vi /usr/include/c++/编译器版本号/exception
class exception{
public:
exception() throw() { }
virtual ~exception() throw();

	/* Returns a C-style character string describing the general cause     
	   of the current error.  */
	virtual const char* what() const throw();
}; 
注:“_GLIBCXX_USE_NOEXCEPT”即为空异常说明“throw()”

eg:
try{
func();

}
catch(exception& ex){//可以捕获exception所有子类类型异常对象
ex.what();
}

6 构造函数和析构函数中的异常 //了解
1)构造函数可以抛出异常,但是对象将被不完整构造,这样的对象其析构不能再被自动调用执行,因此在构造函数抛出异常之前,需要手动清理之前所分配的动态资源.
2)析构函数最好不要抛出异常

二十五 I/O流 //了解

1 主要的I/O流类
ios
/
istream ostream
/ | \ / | \
istrstream ifstream iostream ofstream ostrstream

详细参考官方手册:http://www.cplusplus.com/reference/iolibrary/

2 格式化I/O
1)格式化函数(本质是成员函数)
cout << 100/3.0 << endl;//33.3333
-----------------
cout.precision(10);//格式化函数
cout << 100/3.0 << endl;//33.33333333

2)流控制符(本质是全局函数)
cout << 100/3.0 << endl;//33.3333
------------------
#include
//流控制符
cout << setprecision(10) << 100/3.0 << endl;//33.33333333

3 字符串流
#include //过时,不推荐使用
istrstream、ostrstream

#include //推荐
istringstream //类似sscanf()
ostringstream //类似sprintf()

4 文件流
#include
ifstream //类似fscanf
ofstream //类似fprintf

5 二进制I/O
//类似fread
istream& istream::read(char* buffer,streamsize num);
//类似fwrite
ostream& ostaream::write(const char* buffer,size_t num);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

u.意思

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值