C++知识点大汇总
概述
1、1980年 贝尔实验室 Bjanre Stroustrup(比雅尼·斯特劳斯特鲁普)对C改进与扩充 最初称为“带类的C”,(c with classes). 1983年正式命名为C++
2、
- C++是C的改进与扩充。
- C++包括C的全部属性、特征、优点,是在C的基础上的改进与扩充。
- C++包括过程性语言和类部分。
- C++是混合型语言,即是过程型的,又是面向对象型的。
3、“面向过程”是一种以事件为中心的编程思想。功能分解、行为抽象的抽象编程。
4、面向对象程序设计的基本特征:
(1)对象:
- 数据属性(静态)、行为属性(动态);
- 实现封装与数据隐藏;
- 对象是对类的实例化。
(2)继承:派生其他类
(3)多态:同一个操作在不同的类上有着不同行为
5、编译过程可分为三个子过程:预编译过程、编译过程、连接过程
预编译:预处理源代码中的带有 # 号的语句,生成编译程序可处理的文本文件。(将包含的头文件等以文本的形式展开,插入相应的 cpp 文件)
编译过程:对每个预处理后源程序,编译并生成相应的二进制目标文件(object文件)。此过程中,要求编译器能够识别每个标识符,知道其各自的类型、含义,但不要求知道存放位置。
链接过程:对在整个程序范围内,确定各标志符所代表的地址,如变量、函数入口,生成可执行文件。此过程中,要求编译器能够确定每个标识符所对应的含义或地址。
两个编译工具:
- make:编写程序时常用,条件编译,对每个程序有一个时间戳,只有当前程序包含的程序的时间戳晚于当前程序时才重新编译。
- build:部署之前使用,对程序相关的所有文件编译一遍。
C++工程
main 以及命令行参数
#include <iostream>
using namespace std;
int main(int argc, char *argv[])
{
for (int i = 0; i < argc; i++)
{
cout << "argv[" << i << "] = " << argv[i] << endl;
}
}
// .\p1.exe jlu computer
// argv[0] = D:\study\程序设计\数据结构课设\新\2022-Track B题目(新)\Nogo\p1.exe
// argv[1] = jlu
// argv[2] = computer
头文件
- 标准库头文件与自定义头文件
#include <xx.h> 在系统目录中查找相应的头文件,C 语言风格
#include "xx.h" 现在本地(当前工程)的目录中查找相应的头文件;若找不到,再到系统目录中查找相应的头文件
#include <xx> C++风格,相当于
namespace std {
#include "xx.h"
}
- 前置声明
告诉编译器,一个标识符所代表的类型、含义等,但不必告知具体位置
#include <iostream>
using namespace std;
class A; // 前置声明
class B; // 前置声明
struct A // A 依赖 B(需要提前声明 B)
{
B *b;
};
struct B // B 依赖 A(需要提前声明 A)
{
A *a;
};
int main(int argc, char *argv[])
{
}
- 头文件的使用
头文件的作用:向外声明该头文件对应的 cpp 程序向外公开的变量、函数、结构等
- 包含警戒
保证头文件不被重复定义
作用:允许同一个cpp文件,include多次同一个头文件。但预编译后,各头文件在该cpp中,至多出现一次。
//xx.h
#ifndef XXH
#define XXH
...
#endif
//xx.h
#pragma once
...
基本知识
- 抽象数据类型ADT
一个数学模型 + 可施加其上的操作集合。
(类型名称,数据集,数据间的关系,操作集)
-
OOP语言中的类型
- 基本类型
- 自定义类型
- 泛型:以类型为参数的类型
- 元类型及元对象:各类型的类型(C++不支持)
-
C++ 中的数据类型
- 内置类型:基本类型、扩展类型、
bool
类型 - 自定义类型:使用
typedef
定义的、枚举类型、使用class
、struct
、union
定义的 - 导出类型:数组、指针、引用
- 模板类型
- 内置类型:基本类型、扩展类型、
- 声明和定义
extern
:外部链接声明,不开辟空间,只是声明
- 声明和定义的使用原则
- 就近原则:现使用,现定义
- 先声明后使用
- 单一定义原则:在一个编译单元中,同一个标识符(如变量、函数、类名等)只能被定义一次;但声明可以多次。
- 作用域
- 表达式
注意一下 for 循环中各个表达式的执行顺序:
- 初值表达式(1)
- 条件表达式(2)
- 执行语句(3)
- 步进表达式(4)
- 回到(2)
函数
函数的调用过程
调用约定
所有 windows api 都是使用 _stdcall
内联函数 Inline Function
内联函数也称内嵌或内置函数,它的语法格式与普通函数一样,只是在函数原型或函数定义标题头之前加上关键字inline。
inline int isNumber (char);
inline int isNumber (char ch) {
return(ch >= '0' && ch <= '9') ? 1 : 0;
}
使用内联函数可以省去函数调用所需的建立栈内存环境,进行参数传递,产生程序转移的时间开销。内联函数应是使用频率高,代码却很短的函数。
内联函数的函数体限制:
- 内联函数中,不能含有switch和while。不能存在任何形式的循环语句
- 递归函数不能用来做内联函数。
- 内联函数中不能说明数组。 否则,按普通函数调用那样产生调用代码。 (内联函数是建议性,不是指令性)
- 内联函数只适合于1-5行的小函数
- 类结构中所有在类内部定义的函数,都是内联函数。
- 内联函数中不能有过多的条件判断语句
- 不能对函数进行取址操作
函数重载 Function Overloading
多个同名函数,但它们具有不同参数类型、参数顺序、参数个数、const修饰、异常说明数时,可以同时存在,称为函数重载。
实现函数重载的条件:
- 同一个作用域
- 参数个数不同
- 参数类型不同
- 参数顺序不同
匹配重载函数的顺序:
(1)严格匹配
(2)内部转换(相容类型匹配)
(3)通过用户定义的转换寻找求一个匹配。
编译器如何区分重载函数呢?
C++用名字粉碎(name mangling)(名字细分、名字压轧、名字重整)的方法来改变函数名。
函数重载需要注意的的情况:
-
函数参数的个数、类型、顺序、const修饰、异常等不完全相同
-
返回值类型不能够作为重载依据(区分、细分重载)。
-
缺省值不能作为区分的标志,且编译时匹配。
-
值类型参数的const型与非const型不作为区分标志
-
引用和指针类型参数,是否可以改变实参,可作为区分标志
默认参数的函数
1、当又有声明又有定义时,定义中不允许默认参数。 若只有定义,则默认参数才可出现在函数定义中。
#include <iostream>
using namespace std;
void fun(int a = 20);
void fun(int a = 20) //函数定义处不允许默认参数,应改为 void fun(int a){};
{ cout << "你好" << endl;
}
int main() {
fun(); return 0;
}
#include <iostream>
using namespace std;
void fun(int); //函数声明
int main(){
fun();
return 0;
}
void fun(int a = 20) //函数定义处不允许默认参数,应改为 void fun(int a){};
{
cout << "你好" << endl;
}
2、一个函数中可以有多个默认参数,默认参数应从右至左逐渐定义,当调用函数时,从左向右匹配参数。
void foo(int a, int b = 0, bool c); //错误,b不是最后一参数,只要有一个参数是默认参数,它后面的所有参数都必须是默认参数
3、默认值可以是全局变量、全局常量、函数。不可以是局部变量。因为默认值是在编译时确定的,必须是静态确定的。
4、默认参数不能用于细分重载函数。
void func(int, int);
void func(int = 3, int = 4);
//redefinition of 'void fun(int, int)'
void func(int);
void func(int , int = 4);
//call of overloaded 'fun(int)' is ambiguous
5、函数的定义和声明中,都可以省略形参名
void print (int, int);
void print(int a, int){
cout << a << endl;
}
void func(){
print(1,2);
}
程序运行时的内存布局
代码区:存放程序的执行代码(各个函数的代码块)
全局变量区:放程序的全局数据和静态数据
堆区:存放程序的动态数据malloc free new delete
栈区:存放程序的局部数据(各个函数中的数据)
指针和引用
指针运算符
“ * ”称为指针运算符(间接访问运算符),表示指针所指向的变量的值。一元运算符
“ & ”称为取地址运算符,用来得到一个对象的地址。一元运算符。
数组名是个指针常量,int *const p,指针自身的值是一个常量,不可改变
指针数组和数组指针
- 指针数组:是个数组,每个元素是指针
- 数组指针:是个指针,指向数组
void 指针与NULL指针值
(1)void
- void 指针(void *p) 空类型指针 不指向任何类型,仅仅是一个地址。
- 不能进行指针运算,也不能进行间接引用。
- 其他类型指针可以赋值给空类型指针
- 空类型指针经显示转换后方可赋给其他指针。
(2)NULL
-
本质上是用
#define
定义的数值 0,类型不确定,不推荐使用,推荐使用nullptr
。 -
NULL是空指针值,不指向任何地方。
-
任何类型的指针都可以赋该值。
指针和常量
命名常量可以放在头文件中。
常量折叠:常量所在头文件被其他 cpp 文件包含,在编译期,会直接将常量替换为值,不为其分配空间。如果要访问已经常量折叠的命名常量的值,编译器会临时开辟一个空间,将其限制为常量,不可改,再将地址赋给常量。
(1)使用const 说明常量:
int const x=2;
// or
const int x=2;
常量定义时,应被初始化。
const int i;(错误)
构造函数初始化时必须采用初始化列表一共有几种情况,
- 需要初始化的数据成员是对象(继承时调用基类构造函数)
- 需要初始化const修饰的类成员
- 需要初始化引用成员数据
- 类中含有对象成员,但其类型中,没有无参构造函数,必须在初始化列表中指明相应的有参构造函数。
- 基类中缺少无参构造函数,必须在初始化列表中指明基类的有参构造函数。
(2)指针常量: 在指针定义语句的指针名前加 const
,表示指针本身是常量。 (const 后面是指针)
int a;
int* const p = &a;
定义时必须初始化;
指针值 p
不可以修改,指针指向的内容可以修改。 即 p
是常量,不可以作为左值进行运算,*p
可以修改,p
不可以修改
int* const q = &c; // 错误(不允许外界提供修改常量的漏洞)
(3)常量指针: 在指针的定义类型前加 const
,表示指向的对象是常量。 (const 后面是指针指向的内容)
const int* p
// or
int const* p
以上定义表明,*p
是常量,不能将 *p
作为左值进行操作。
定义指向常量的指针时,定义时不必须先初始化。*p
不可以修改,p
可以修改
(4)指向常量的指针常量(常量指针常量)
const int *const p = &a;
定义时必须初始化 p与 *p 都是常量。他们都不能作为左值进行操作
(5)常量的特殊用法:int f(int b) const;
(6)重载和 const 形参
void f(int* p);
void f(const int* cp);
//有效重载,是不是指向const对象
void f(int* p );
void f(int * const pc);
//无效重载,重定义不能基于指针本身是否const 实现重载
(7)new 和 delete进行动态内存分配和释放
int *p; p=new int (100);//动态分配一个整数并初始化
delete p;
int *p;p=new int[10]; //分配一个含有10个整数的整形数组 delete[ ] p; //删除这个数组
使用new较之使用malloc()有以下的几个优点:
- new自动计算要分配类型的大小,不使用sizeof运算符,比较省事,可以避免错误。
- 自动地返回正确的指针类型,不用进行强制指针类型转换。
- 可以用new对分配的对象进行初始化。
- 不用使用头文件声明(malloc.h),更简洁。
new操作符若申请成功,返回首单元地址;否则返回NULL值。
关于字符串的说明
第一种方式:char * str
会将字符串放在常量数据区,等价于常量指针,指向的内容不能修改。
函数指针和指针函数
(1)指针函数:返回指针值的函数。
int* f(int a);
(2)函数指针:指向函数地址(程序区)的指针。与函数名等价的指针。函数名是指向函数的指针常量。
//类型 (*变量名) (形参表);
//()的优先级大于*
int f1(int n);
int (*pf1)(int n);
pf1=f1; //pf1是与f1等价的函数指针
(3)通过函数指针来调用函数:
int add(int a,int b){return a+b;}
int main()
{
int (*p)(int,int);
p=add; //p是与add等价的函数指针
cout<<add(3,5);
cout<<(*p)(3,5); //四种调用形式效果等价
cout<<p(3,5);
cout<<(*add)(3,5);
return 0;
}
//结果:8888
(4)函数指针作函数形参:
例:计算以0.10为步长,特定范围内的三角函数之和。
#include <iostream>
using namespace std;
int sum(int a, int b){
return a + b;
}
int sub(int a, int b){
return a - b;
}
int get(int (*p)(int a, int b), int m, int n){
return p(m, n);
}
int main(){
cout << "sum=" << get(sum, 3, 3) << endl;
cout << "sub=" << get(sub, 3, 3) << endl;
return 0;
}
//sum=6
//sub=0
(5)用typedef 来简化函数指针
typedef int (*FUN)(int a,int b);
int f(int,int);
FUN funp=f;
//FUN 不是指针变量,是指针类型名。
typedef int FUNC(int,int); //先定义函数类型
FUNC* funp=f;
例子:
#include <iostream>
using namespace std;
int sum(int a, int b){
return a + b;
}
int sub(int a, int b){
return a - b;
}
int get(int (*p)(int a, int b), int m, int n){
return p(m, n);
}
typedef int (*Func)(int a, int b);
typedef int Func2(int a, int b);
int main(){
Func func = sum;
Func2 *func2 = sub;
cout << "sum=" << get(func, 3, 3) << endl;
cout << "sub=" << get(func2, 3, 3) << endl;
return 0;
}
(6)函数的返回类型可以是函数指针
typedef int (*SIG) ();
typedef void (*SIGARG) ();
SIG signal (int,SIGARG);
引用
(1)为一个变量、函数等对象规定一个别名,该别名称为引用。
(2)声明引用:
int& ir = i; //定义引用ir作为对象i的别名
声明引用,不为之分配内存空间。
(3)引用必须初始化。引用一旦被声明则不能再修改。
int j, k;
int &s = j;
int &s = k; //(错误)
int & i; //错误
extern int& r3 //ok,r3在别处初始化
void &a = 3;//error
//void 本质不是一个类型,只是在语法上相当于一个类型,没有该类型的对象。
(4)形参和实参结合规则:
形参为引用时,凡遇到形参(引用)的地方,全部用实参(对象)来代替。
可读性比指针传递好(与传值方式的调用可读性相同,性能却强于传值方式)
可使用引用传递从函数返回多个值(指针和引用都可以)
(5)引用和指针的关系
指针是个变量,可以声明但不初始化,可再赋值; 而引用建立时必须进行初始化并且决不会再关联其它不同的变量。
指针操纵两个实体(指针值、指向的值);引用只能操纵一个实体。
引用在内部用指针实现 ,被看成是指针常量,不能操作自身的地址值,只能访问所指向的实体。
实际上“引用”可以做的任何事情“指针”也都能够做,为什么还要“引用”?
答案是:
“用适当的工具做恰如其分的工作”。 指针能够毫无约束地操作内存中的东西,尽管指针功能强大,但是非常危险。引用是指针出于安全考虑的替代品。 高级编程多用引用,低级编程多用指针,也是基于安全考虑。
其价值体现在,调用方不需要知道变量的地址,就可以传参,由函数进行变量值的修改。
在以下情况下你应该使用指针:
一是你考虑到存在不指向任何对象的可能(在这种情况下,你能够设置指针为空)
二是你需要能够在不同的时刻指向不同的对象(在这种情况下,你能改变指针的指向)。
如果总是指向一个对象并且一旦指向一个对象后就不会改变指向,那么你应该使用引用。
(6)用 const 限定引用
int i = 1;
const int& p = i;
const int& t = 1; // 对字面量只能使用常量型引用,不能 int& t = 1;
不能通过引用对目标变量的值进行修改,保证了引用的安全性。
注意:c++不分变量的 const 引用,和 const 变量的引用,因为引用本身就不能重新赋值,使它指向另一个变量。 即没有 const int const &a=1,只有 const int &a=1
(7)引用的使用——用引用返回值
float& f2(float r) {
t = 3.14 * r * r;
return(t);
}
用引用返回一个函数值的最大好处是:在内存中不产生被返回值的副本。
不要返回一个局部对象的引用。
(8)引用的使用——函数调用作为左值
练习:
char a[] = "abcdefghij";
char *q = a;
int *p = (int*) a;
while (*q)
*q++ = *q + 1; //++运算符优先于* 等价于*(q++)=*q+1; *a=bcdefghijk
p += 2; //char的大小是1字节,int的大小是4字节,p移动了8字节
printf("%s",p); //sizeof(char)=1 //sizeof(int)=4
//答案是jk
类
从结构到类
C++采用“类”来支持对象,同类对象实体抽象出其共性,形成类(数据类型),类封装了数据与处理数据的过程(函数)
C++的类中既能包含数据成员,又包含函数成员或称成员函数。
一般而言,类中的数据成员在定义类的时候是不能初始化的。
class class_name_identifier
{
public: //访问控制符
公共数据成员声明语句;
公共函数成员声明语句;
private:
私有数据成员声明语句;
私有函数成员声明语句;
protected :
保护数据成员声明语句;
保护函数成员声明语句;
};
C++中类与结构的区别: 默认情况下,class定义的成员是private的,而struct 定义的成员是public的。
对象的引用以及返回对象的函数
void add (Student &b){
//函数体
}
Student get(){
//函数体 Student tmp; return tmp
}
公有&保护&私有成员
访问权限 | 类内成员函数或者数据成员 | 派生类 | 类外 | 友元函数 |
---|---|---|---|---|
public | yes | yes | yes | yes |
protected | yes | yes | no | yes |
private | yes | no | no | yes |
成员函数&非成员函数
成员函数属于类,成员函数定义是类设计的一部分, 其作用域是类作用域.,而普通函数一般为全局函数
成员函数的操作主体是对象,使用时通过捆绑对象来行使其职责,,而普通函数被调用时没有操作主体
重载成员函数
在类内重载成员函数与普通函数重载一样
定义成员函数
(1)在类的内部定义成员函数
class A{
private:
int a;
public:
void show(){
cout << "haha" << endl;
}
};
::是作用域区分符、作用域运算符、名字空间引导符
单独用::表示全局变量和函数
#include <iostream>
using namespace std;
int month = 10;
void show(){
cout << "show" << endl;
}
class A{
public:
void show(){
::show(); //调用全局函数
}
};
int main(){
int month = 5;
cout << "局部month=" << month << endl;
cout << "全局month=" << ::month << endl;
A a;
a.show();
return 0;
}
(2)在类之后定义成员函数
class A{
private:
int a;
public:
void show();
};
void A::show(){
cout << "haha" << endl;
}
程序结构
(1)文件结构
1、一般情况下, c++的类定义和成员函数定义分离
< >:用于c++提供的头文件,一般存放在c++系统目录中的include 子目录下
“ ”:首先在当前文件所在的目录中进行搜索,找不到,再按标准方式进行搜索.
2、类定义和使用的分离
成员函数的实现和类外定义不要一起放在头文件中
外部链接:函数定义,可以在其他文件中使用。不能重复定义。
内部链接:只能在自己的程序中使用,可以跨文件重复定义。如:类型定义、全局常量、inline函数、模板定义等。
3、头文件卫士
Student.h文件
#ifndef STUDENT
#define STUDENT
class Student{
public:
void p(){
cout<<score;
}
float score;
protected:
char name;
int age;
};
#endif
头文件卫士保证头文件在一个程序文件中只被定义一次
(2)c++程序中的函数的组织方式
头文件的使用:使函数调用免于声明
// a1.h a1.cpp提供的资源
void f1();
// a2.h a2.cpp提供的资源
void p();
// a3.h a3.cpp提供的资源
void g1();
void g2();
void f2();
void h();
// s.cpp
#include”a2.h”
#include”a3.h”
void s()
{
if(…){
p();
g1();
}else{
g2();
h();
}
}
成员函数的显式内联
class Date {
int year, month, day;
void set ( int y, int m, int d ) { // 默认内联
year=y; month=m; day=d;
}
bool isLeapYear ( );
};
inline bool Date::isLeapYear ( ) // 显式内联
{ return ! ( year%400 ) || !(year%4) &&( year%100);
}
//类体外定义一目了然 增强可读性
//必须将类定义和成员函数的定义都放在同一个头文件或者同一个源文件中,否则编译时无法置换。
this 指针
this是个指针常量
this是对当前对象的引用
this 指针 是一个隐含于每一个类的成员函数中的特殊指针(包括析构函数和构造函数),它用于指向正在被成员函数操作的对象。
程序中可显式使用this指针
#include <iostream>
using namespace std;
class A{
public:
int age = 0;
void add(){
this->age++;
}
A &p(){
return *this;
}
};
int main(){
A a;
(a.p().age)++;
cout << a.age;
return 0;
} //1
类的封装
首先是数据与算法(操作)结合,构成一个不可分割的整体。
其次是在这个整体中一些成员是保护的,他们被有效地屏蔽,以防外界的干扰和误操作。
另一些成员是公共的,他们作为接口提供给外界使用。
保护(私有)成员protected(private):
(1)保护类的内部数据不被肆意侵犯
(2)是类对它本身内部实现的维护负责,因为只有类自己才能访问该类的保护数据,所以对一切保护数据的维护只有靠类自己。
(3)限制类与外部世界的接口。 保护成员对使用者不可见。
(4)减少类与其它代码的关联程度。类的功能是独立的,不依赖于应用程序的运行环境。
公共成员(公共接口、对外接口)(public):
需要让外界调用的成员函数指定为公共的,外界通过公共的函数来实现对数据的操作。
屏蔽类的内部实现
对类内所有的private修饰的数据提供get和set接口给外界,避免外界对数据直接进行操作。在java里叫javabean类
class A{
private:
int age = 0;
public:
int getAge(){
return this->age;
}
void setAge(int age){
this->age=age;
}
};(2)类的作用域
作用域
(1)变量的作用域-{}内或其后的全部内容
(2)类的作用域
包括类定义作用域和类实现作用域
类定义作用域:一个类的成员函数对同一类的数据成员具有无限制的访问权。 私有成员和受保护成员只能被类内部的成员函数访问; 公有成员是类提供给外部的接口, 可以在类外部被访问. 这种技术实现了信息的隐藏和封装.
类的实现作用域:通过类的对象体现,对象有全局对象、局部对象等作用范围。
可见性:
- 当内层的变量和外层的变量同名时,在内层里,外层的变量暂时地失去了可见性。
- 不能在同一作用范围内有同名变量
类名允许与其它变量名或函数名相同
1)如果一个非类名隐藏了类名,则类名通过加前缀即可
class Sample
{//…..};
void fun(int Sample)
{ class Sample s; //定义一个对象要加前缀
Sample++;
//…}
2)如果一个类名隐藏了非类名,则用一般作用域规则即可
int s=0;
void func()
{ class s{//…};
s a:
::s=3;} // class作用域到此结束
int g=s; //在函数中定义的类称为局部类,局部类的成员函数必须在类定义内部定义
生命期(生存期)
指一个对象产生后,存活时间的度量。
在生存期内,对象保持它的状态。
作用域与生命期不尽相同。
整个程序的生命期:全局(静态)数据
静态生命期:静态局部数据
局部生命期:
动态生命期:由new申请到内存空间之后,该空间实体开始有效,一直到delete释放该内存空间
名字空间
名字空间的作用是建立一些互相分隔的作用域,把一些全局实体分隔开来,以免产生名字冲突。
避免不同人编写的程序组合时会遇到名字冲突的危险
可以包含:变量&对象;常量;函数定义; 类型(结构)定义;名字空间的定义
不能包含:预处理命令 ;
可以开放定义;可以{}外定义
#include <iostream>
using namespace std;
namespace name
{
int a = 20;
void show()
{
cout << "I'm a namespace" << endl;
}
namespace another
{
int b = 30;
}
}
int main()
{
cout << name::a << endl;
name::show();
cout << name::another::b << endl;
return 0;
}//20//I'm a namespace//30
标准c++库的所有标识符都是在一个名为std的名字空间定义的。
using namespace std;
//在std中定义和声明所有标识符都可以作为全局量来使用。
面向对象基本概念
对象:物理实体在计算机逻辑中的映射和体现
类:同种对象的集合与抽象
实体:现实世界中需要描述的对象
面向对象:
就是力图从实际问题中抽象出封装了数据和操作的 对象,通过定义对象的各种属性来描述他们的特征和功能,通过接口的定义描述他们的地位及与其它对象的关系,最终形成一个广泛联系的可理解、 可扩充、可维护、更接近于问题本来面目的动态对象模型系统。
OOA:面向对象分析(干什么)
OOD:面向对象设计(怎样干)
OOP:面向对象编程(实现)
构造、析构函数
构造、析构函数
构造函数:
- 构造函数是特殊的类成员函数。
- C++规定与类同名的成员函数是构造函数,在该类的对象创建时,自动被调用。
- 构造函数负责对象的初始化 可拥有多个参数。
- 可以重载。
- 构造函数不返回具体的值,不指定函数的返回类型。
- 可内置定义,也可以在类外定义。
- 通常是Public的
析构函数:
- 析构函数也是一个特殊的成员函数;
- 作用与构造函数相反;
- 在对象的生命期结束时自动被调用。
- 名字是类名前加一个~;
- 不返回任何值;没有函数类型,没有函数参数,因此不能被重载。
- 一个类可以由多个构造函数,只能有一个析构函数。
- 可内置定义,也可以在类外定义。
- 用于清理和释放资源的工作。
- 通常是Public的
- 析构函数并不是销毁对象的,只是释放构造函数在构造时初始化的资源(主要包括堆上分配内存等)
#include <iostream>
using namespace std;
class A{
private:
int a;
public:
A(){
cout << "Default constructor called" << endl;
}
A(int a){
this->a = a;
cout << "Constructor called" << endl;
}
~A(){
cout << "Destructor called" << endl;
}
};
int main(){
A a;
A b(1);
return 0;
}
//Default constructor called
//Constructor called
//Destructor called
//Destructor called
组合类
#include <iostream>
using namespace std;
class Student{
public:
Student(){
cout << "Student constructor called" << endl;
}
~Student(){
cout << "Student destructor called" << endl;
}
};
class Teacher{
public:
Teacher(){
cout << "Teacher construtor called" << endl;
}
~Teacher(){
cout << "Teacher destrctor called" << endl;
}
};
class Manager{
private:
Student student;
Teacher teacher;
public:
Manager(){
cout << "Manager constructor called" << endl;
}
~Manager(){
cout << "Manager destructor called" << endl;
}
};
int main(){
Manager manager;
return 0;
}
//Student constructor called
//Teacher construtor called
//Manager constructor called
//Manager destructor called
//Teacher destrctor called
//Student destructor called
//注意执行顺序,组合类的对象成员先初始化,调用构造函数,然后是本类初始化,调用构造函数,析构顺序相反
#include <iostream>
using namespace std;
class Student{
public:
Student(){
cout << "Student constructor called" << endl;
}
~Student(){
cout << "Student destructor called" << endl;
}
};
class Teacher{
public:
Teacher(){
cout << "Teacher construtor called" << endl;
}
~Teacher(){
cout << "Teacher destrctor called" << endl;
}
};
class Temp{
public:
Temp(){
cout << "Temp constructor called" << endl;
}
~Temp(){
cout << "Temp destructor called" << endl;
}
};
class Manager : public Temp{
private:
Student student;
Teacher teacher;
public:
Manager(){
cout << "Manager constructor called" << endl;
}
~Manager(){
cout << "Manager destructor called" << endl;
}
};
int main(){
Manager manager;
return 0;
}
//Temp constructor called
//Student constructor called
//Teacher construtor called
//Manager constructor called
//Manager destructor called
//Teacher destrctor called
//Student destructor called
//Temp destructor called
当一个类既是组合类又是派生类,它在创建对象时,系统对构造函数的调用顺序有相应的规定:
最先调用基类的构造函数,初始化基类的数据成员;
然后调用子对象所在类的构造函数,初始化子对象的数据成员;
最后调用本类的构造函数,初始化新增数据成员。
#include <iostream>
using namespace std;
class Student{
public:
Student(){
cout << "Student constructor called" << endl;
}
~Student(){
cout << "Student destructor called" << endl;
}
};
class Teacher{
public:
Teacher(){
cout << "Teacher construtor called" << endl;
}
~Teacher(){
cout << "Teacher destrctor called" << endl;
}
};
class Temp
{
public:
Temp(){
cout << "Temp constructor called" << endl;
}
~Temp(){
cout << "Temp destructor called" << endl;
}
};
class Temp2{
public:
Temp2(){
cout << "Temp2 constructor called" << endl;
}
~Temp2(){
cout << "Temp2 destructor called" << endl;
}
};
class Manager{
private:
Student student;
Teacher teacher;
public:
Manager(){
Temp temp; //局部对象Temp
Temp2 temp2; //局部对象Temp2
cout << "Manager constructor called" << endl;
}
~Manager(){
cout << "Manager destructor called" << endl;
}
};
int main(){
Manager manager;
return 0;
}
/*
Student constructor called
Teacher construtor called
Temp constructor called
Temp2 constructor called
Manager constructor called
Temp2 destructor called
Temp destructor called
Manager destructor called
Teacher destrctor called
Student destructor called
*/
如果类内有局部临时对象:
则先初始化类内子对象,再初始化局部临时对象,最后初始化本类对象。
析构时先调用局部临时对象的析构函数,且多个局部临时对象的构造和析构顺序相反
引用应依附于另一个独立的变量,等待初始化。
常量数据成员是仅仅初始化一次,其值便不再改变的数据成员。
二者只能借助冒号语法初始化。
#include <iostream>
using namespace std;
class A
{
private:
const int a; //常数据成员
int &b; //引用
public:
A(int a, int b) : a(a), b(b)
{
cout << "A constructor called" << endl;
} /*A(int a,int b){ //错误 this->a=a; this->b=b; }*/
};
int main()
{
A a(10, 20);
return 0;
}
对象构造顺序
局部和静态局部对象(静态生命期)以文本定义顺序为顺序 (类成员属于此种情况)
静态对象在首次定义时构造一次;程序结束析构
全局对象在main之前构造;程序结束时析构
全局对象如果分布在不同文件中,则构造顺序随机
常对象和常成员函数
常对象:数据成员值在对象的整个生存期间内不能被改变。
即常对象定义是必须进行初始化,而且不能被更改。
声明的语法形式: const 类名 对象名 或者 类名 const 对象名
#include <iostreamusing namespace std;
class A{
private:
const int a;
int &b;
public:
A(int a, int b) : a(a), b(b){}
void show() const; //构成重载
void show() //如果不是常对象,则优先调用普通函数
{
cout << "normal show" << endl;
}
};
void A::show() const //类外实现常成员函数时不可省略const
{
cout << "const show" << endl;
}
int main(){
A a(10, 20);
a.show(); //normal show
const A b(10,20);
b.show(); //const show
return 0;
}
没有常构造函数和常析构函数,常对象和普通对象都调用同一构造函数和析构函数。
为对象申请动态空间
int *p=new int;
delete p;
int *q=new int[10];
delete []q;
函数内部的申请空间要及时释放,否则容易造成内存重复申请和内存迷失(内存泄漏)
对象数组不能通过参数传递初始化。要么默认构造函数,要么构造函数有默认参数。
拷贝构造函数
一个已知对象构造(初始化)另一对象
类名 (类名& 形参); 或者是 类名 ( const 类名& 形参);
Student (Student& s);或者是Student ( const Student& s);
一旦提供了拷贝构造函数,就不在提供默认构造函数
#include <iostream>
using namespace std;
class B{
private:
int a;
public:
B(const B &b) {}
};
int main(){
B b;
return 0;
}
//no matching function for call to 'B::B()'
浅拷贝:
创建q时, 对象p被复制给了q, 但资源未复制, 使得p和q指向 同一个资源, 这称为浅拷贝。
可能会破坏该堆及自由内存表
深拷贝:
复制指针的同时,开辟同样的空间,把空间的数据复制,让指针分别指向各自的内存空间
class aa {
public:
aa(){f=new char[10];}
aa(aa const & s){
f=new char[10]; //开辟空间 strcpy(f,s.f);} //数据复制
~aa(){delete [ ]f;}
char * f;
};
int main()
{aa p;
strcpy(p.f,"Computer");
cout<<p.f<<endl;
aa q(p);
cout<<q.f<<endl;
strcpy(p.f,”Software”);
cout<<p.f<<q.f<<endl;
return 0;}
如果派生类调用拷贝构造函数且基类含有拷贝构造函数,则基类拷贝构造函数也会被调用
#include <iostream>
using namespace std;
class Data
{
public:
Data(int x = 0)
{
Data::x = x;
cout << "Data()" << endl;
}
Data(const Data &)
{
cout << "Data(const Data&)" << endl;
}
~Data()
{
cout << "~Data()" << endl;
}
private:
int x;
};
class Base
{
public:
Base()
{
cout << "Base()" << endl;
}
Base(const Base &)
{
cout << "Base(const Base&)" << endl;
}
virtual ~Base() {}
};
class Child : public Base
{
public:
Child()
{
cout << "Child()" << endl;
}
virtual ~Child()
{
cout << "~Child()" << endl;
}
private:
Data d2;
};
int main()
{
Child c1;
Child c2(c1);
return 0;
}
/*
Base()
Data()
Child()
Base(const Base&)
Data(const Data&)
~Child()
~Data()
~Child()
~Data()
*/
关于无名对象的构造与使用:
构造函数:Student(int i){ }
Student(9);// 无名对象 ,正确
Student *p=&Student(8);//错误,error: taking address of temporary
Student const &q=Student(7);//正确,无名对象将在它的引用离开作用域时销毁
Student i(6);
i = Student(5);//正确 Student &q=Student(5); //错误cannot bind non-const lvalue reference of type 'Student&' to an rvalue of type 'Student'
构造函数用于类型转换
class aa {
public:
aa(int a=1){id=a;cout<<“构造”<<endl;}
aa (aa & s){id = s.id;cout<<“拷贝”;}
int & Getid(){return(id);}
private:
int id;
};
int main()
{ aa m; //“构造”
aa n(m); //“拷贝”
aa o = m; //“拷贝” 和aa o(m)等价
aa s = 9; //“构造” 和aa s(9)等价
aa t; //“构造”
t = 9; // t=aa(9);“构造”、 赋值运算
return 0;
} //隐式的类型转换,可以将其他类型转换成类类型
转换不能太复杂,不允许多参数,不允许间接转换
class aa {
public:
aa(int a=1){id=a;}
int id;
};
class bb {
public:
bb(int a=2){id=a;}
int id;
};
void m(aa a)
{cout<<a.id<<endl;}
void m(bb a)
{cout<<a.id<<endl;}
int main()
{ m(9); //存在二义性; m(bb(9));
return 0;
}//转换不能太复杂,不允许多参数,不允许间接转换
explicit可以禁止隐式类型转换
explicit A(int a){this->a=a}
A a=3; //错误error: conversion from 'int' to non-scalar type 'A' requested
A a(3); //正确
友元
(1)声明的位置既可在public区,也可在protected区。友元函数虽然是在类内进行声明,但它不是该类的成员函数,不属于任何类。
(2)在类外定义友元函数,与普通函数的定义一样,一般与类的成员函数放在一起,以便类重用时,一起提供其友元函数。
(3)友元函数是能访问类的所有成员的普通函数,一个函数可以是多个类的友元函数,只需在各个类中分别声明。
(4)友元能使程序精炼,提高程序的效率。
(5)友元破坏了类的封装,使用时,要权衡利弊,在数据共享与信息隐藏之间选择一个平衡点。
(6)友元类的所有成员函数都可视为该类的友元函数,能存取该类的私有成员和保护成员。
(7)友元关系不具有对称性。 友元关系不具有传递性。如果类B是类A的友元类,类C是类B的友元类,这并不隐含类C是类A的友元类
#include <iostream>
#include <string>
using namespace std;
class Building;
//友元类
class MyFriend
{
public:
//友元成员函数
void LookAtBedRoom(Building &building);
void PlayInBedRoom(Building &building);
};
class Building
{
//全局函数做友元函数
friend void CleanBedRoom(Building &building);
#if 1
//成员函数做友元函数
friend void MyFriend::LookAtBedRoom(Building &building);
friend void MyFriend::PlayInBedRoom(Building &building);
#else
//友元类
friend class MyFriend;
#endif
public:
Building();
public:
string mSittingRoom;
private:
string mBedroom;
};
void MyFriend::LookAtBedRoom(Building &building)
{
cout << "我的朋友参观" << building.mBedroom << endl;
}
void MyFriend::PlayInBedRoom(Building &building)
{
cout << "我的朋友玩耍在" << building.mBedroom << endl;
}
//友元全局函数
void CleanBedRoom(Building &building)
{
cout << "友元全局函数访问" << building.mBedroom << endl;
}
Building::Building()
{
this->mSittingRoom = "客厅";
this->mBedroom = "卧室";
}
int main()
{
Building building;
MyFriend myfriend;
CleanBedRoom(building);
myfriend.LookAtBedRoom(building);
myfriend.PlayInBedRoom(building);
system("pause");
return EXIT_SUCCESS;
}
/*
友元全局函数访问卧室
我的朋友参观卧室
我的朋友玩耍在卧室
*/
静态成员
前面没有static ,类外初始化
int StudentID::ID=0;
(1) 不管一个类的对象有多少个,其静态数据成员也只有一个,由这些对象所共享,可被任何一个对象所访问。
(2) 在一个类的对象空间内,不包含静态成员的空间,所以静态成员所占空间不会随着对象的产生而分配,或随着对象的消失而回收。
(3) 静态数据成员的存储空间的分配是在程序一开始运行时就被分配。并不是在程序运行过程中在某一函数内分配空间和初始化。
(4) 静态数据成员的初始化语句,既不属于任何类,也不属于包括主函数在内的任何函数,静态数据成员初始化语句最好在类的实现部分定义
(5) 对于在类的public部分说明的静态数据成员,可以不使用成员函数而直接访问,既使未定义类的对象,同样也可以直接访问,但在使用时也必须用类名指明所属的类。 而private和protected部分的静态成员只能通过类的成员函数访问。
(6) 不允许不指明对象访问非静态数据成员;不允许使用this
(7)可以用const限定静态成员吗?
可以限定形态数据成员,不可以限定静态成员函数
class A{
public:
const static int a;
static void getA() const{ //编译报错
return a;
}
};
//静态成员函数上不允许使用类型限定符
class A{
private:
int t = 10;
public:
const static int a;
static int getT(){
return t;
}
};
//error: invalid use of member 'A::t' in static member function非静态成员引用必须与特定对象相对
单例模式
懒汉式:
#include <iostream>
using namespace std;
class A{
private:
A() {} //构造函数私有化
A(const A &a) {}
static A *instance;
public:
void show(){
cout << "hello" << endl;
}
static A *getInstance(){
if (instance == NULL){
instance = new A;
}
return instance;
}
};
A *A::instance = NULL; //静态数据成员类外初始化
int main(){
A *instance = A::getInstance();
instance->show();
return 0;
}
//如果写A a会报错
运算符重载
格式
使用运算符重载的一般格式为:
类型名 operator 运算符(形参表) {函数体}
其中operator是关键字,类型名为重载运算符的返回类型,即运算结果类型。
(1)在C++中几乎所有的运算符( 除“.”(成员选择符)、“.*”(成员对象选择符)、“->*(成员指针选择符)”、“::”、“?:”、“size of”外)都可以被重载。 只能重载 C++中已有的运算符,不允许创建新的运算符.
(2) 运算符的重载既不会改变原运算符的优先级和结合性。
(3) 至少有一个操作对象是自定义类型,参数都是基本类型时不能重载.
(4) 不提倡改变参数个数、运算符含义。
(5) 重载运算符的函数不能有默认的参数。
(6) 运算符重载时参数个数不可以超过原来数目
赋值运算符重载
拷贝构造函数和赋值操作符都是用来拷贝一个类的对象给另一个同类型的对象。
void fn(A a) {
A na=a; //拷贝构造函数:将一个对象拷贝到另一个新对象
A b; b=a;
} //赋值运算符:将一个对象为另一个已经存在的对象赋值
编译器提供默认的拷贝构造函数和赋值运算符的运作机制。
就是将对象中的每个数据成员拷贝到目标对象相应的数据成员中。
若类的数据成员中有指向动态分配空间的指针,通常定义拷贝构造函数,此时,应重载赋值运算符。
实现深拷贝
类外显式调用赋值函数
#include <iostream>
using namespace std;
class A
{
public:
int num;
A(int n) : num(n) {}
};
int main()
{
A a(3);
A b(0);
b.operator=(a);
cout << b.num;
return 0;
}
//3
++/--运算符重载
类型转换运算符
类型转换函数的作用是将一个类的对象转换成另一类型的数据
类型转换运算符声明的形式:
operator 类型名();
没有参数,没有返回类型,(其返回类型由函数名字指定)但是函数体中必须包含return语句。
只能作为成员函数。
同一个类中不能定义多个转换运算符重载函数.
#include <iostream>
using namespace std;
class aa{
float a;
float b;
public:
float &aaa() { return (a); }
float &bbb() { return (b); }
operator float();
};
aa::operator float() { return (a); }
int main(){
aa a, b;
a.aaa() = 1;
a.bbb() = 2;
b.aaa() = 3;
b.bbb() = 4; //int(3.5)
cout << float(a) << endl; //a.operator float();
cout << 10 + a << endl;
cout << a + b << endl;
cout << a << endl;
}
类和友元
重载<<运算符
class Person{
friend ostream& operator<<(ostream& os, Person& person);
public:
Person(int id,int age){
mID = id;
mAge = age;
}
private:
int mID;
int mAge;
};
ostream& operator<<(ostream& os, Person& person){
os << "ID:" << person.mID << " Age:" << person.mAge;
return os;
}
//全局函数友元
class Person
{
public:
Person(int id, int age)
{
mID = id;
mAge = age;
}
ostream &operator<<(ostream &os)
{
os << "ID:" << mID << " Age:" << mAge;
return os;
}
private:
int mID;
int mAge;
};
//另一种:在类内重载<<,不需友元
继承
class 派生类名:继承方式 基类名
{ 派生类中的新成员 }
基类(父类)、派生类(子类)class Master :public Student
派生类继承了基类的除了构造函数、析构函数、拷贝构造函数和赋值运算符重载函数之外的所有成员,因此派生类对象由两部分组成:一部分是由基类继承的成员,另一部分是派生类新增加的自己特有的成员。
当类的继承方式为公有继承时,基类的公有和保护成员的访问属性在派生类中不变,而基类的私有成员不可访问。
直接基类:直接参与派生出某类的基类。间接基类:基类的基类,甚至更高层的基类。
继承的本质实际上就是由上到下完全的复制;但是在对内可见性上做了手脚,对外可见性则没有改变。
c++提供了类的继承机制,解决了软件的复用问题。
赋值兼容规则是指需要基类对象的任何地方都可以使用公有派生类的对象来替代。替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员。
里氏代换原则: (LSP-Liskov Substitution Principle) 在软件里面,把基类都替换成它的子类,程序的行为没有变化。
使用时还应注意:基类指针指向派生类对象时,只能通过基类指针访问派生类中从基类继承来的成员,不能访问派生类中的其它成员。
不允许将基类的对象赋值给派生类的对象
当类的继承方式为保护继承时,基类的公有和保护成员都以保护成员身份出现在派生类中,而基类的私有成员不可访问。
当类的继承方式为私有继承时,基类的公有和保护成员都以私有成员身份出现在派生类中,而基类的私有成员不可访问。
protected继承和private继承得到的类都不是子类 “凡是父类对象可以出现的地方可以用子类对象代替”,不再适用
#include <iostream>
using namespace std;
class Parent{};
class Child : protected Parent{};
int main(){
Child child;
Parent &parent = child;
return 0;
}
//error: 'Parent' is an inaccessible base of 'Child'
//不允许对不可访问的基类 "Parent" 进行转换
继承类型省略时默认为私有继承
派生类中初始化基类数据成员,不可以调用基类的构造函数,可以使用初始化成员列表
#include <iostream>
using namespace std;
class Parent{
private:
int a;
public:
Parent(int a){
this->a = a;
}
};
class Child : protected Parent{
public:
Child(int a) : Parent(a) //正确
{
//Parent(a)错误no matching function for call to 'Parent::Parent()
}
};
int main(){
return 0;
}
派生类构造函数执行的一般顺序是:
(1)基类构造函数,
(2)派生类对象成员类的构造函数(如果有的话)。
(3)派生类构造函数体中的内容。
析构函数的执行顺序相反
嵌套类
在一个类中定义的类称为嵌套类,定义嵌套类的类称为外围类。定义嵌套类的目的在于隐藏类名,减少全局的标识符,从而限制用户能否使用该类建立对象。这样可以提高类的抽象能力,并且强调了两个类(外围类和嵌套类)之间的主从关系。
class A{
int a;
public:
class B{
int b;
public:
B(int i);
};
void f(){ pb->b=5; //error}
B* pb;
};
A::B::B(int i):b(i){} //成员可以在体外定义
int main(){
A a;
A::B b(10);
}
嵌套类中说明的成员不是外围类中对象的成员,反之亦然。
嵌套类的成员函数对外围类的成员没有访问权,反之亦然。
嵌套类仅仅是语法上的嵌入。 在嵌套类中说明的友元对外围类的成员没有访问权。
继承和组合的关系
组合是“has a”关系的模型。 汽车=方向盘+轮子+车窗+……
继承是"is a"关系模型。是为了产生子类型。让开发人员设计“kind of”关系的模型。 鸟类->老鹰
组合和继承不是绝对的。组合可以用继承来实现,继承也可以由组合来实现。
避免继承带来的重负:继承是C++中第二紧密的耦合关系,仅次于友元关系。紧密的耦合是一种不良现象,应该尽量避免。因此,应该用组合代替继承,除非知道后者确实对设计有好处。人们曾经过度地使用继承,即使是有经验的程序员也会如此。软件工程的一条明智原则,就是尽量减少耦合:如果一种关系不只有一种表达方式,那么就 应该用最可行的最弱关系。考虑到继承关系几乎是C++中所能表达的最强关系,因此只有在没有更弱的等价代替选择时,才适合使用。如果用组合就能表示类的关系,那么应该优先使用。
所以更加严格的继承规则应当是:若在逻辑上B是A的“一种”,并且A的所有功能和属性对B而言都有意义,则允许B继承A的功能和属性。
多重继承和虚基类
若一个派生类具有两个或两个以上基类,这种继承称为多重继承。
class M_p:public Master,public Phd{}
多继承派生类构造函数执行顺序是:
(1) 所有基类的构造函数;多个基类构造函数的执行顺序取决于定义派生类时所指定的顺序,与派生类构造函数中所定义的成员初始化列表的参数顺序无关。
(2) 对象成员(如果有的话)的构造函数;
(3) 派生类本身构造函数的函数代码。
虚基类
格式如下:
class 派生类名:virtual public 基类名 { //声明派生类成员 };
这时,从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。
当基类通过多条派生路径被一个派生类继承时,该派生类只继承该基类一次,即基类成员只保留一次。
C++规定,虚基类子对象是由最后派生类的构造函数通过调用虚基类的构造函数进行初始化的。
如果一个派生类有一个直接或间接的虚基类,那么派生类的构造函数的成员初始化列表中必须列出虚基类构造函数的调用.
如果没有列出,则表示使用该虚基类的缺省构造函数来初始化派生类对象中的虚基类子对象。
多继承的构造顺序:
(1)任何虚拟基类的构造函数按照他们被继承的顺序构造
(2)任何非虚拟基类的构造函数按照他们被继承的顺序构造
(3)任何成员对象的构造函数按照他们被声明的顺序构造
(4)类自己的构造函数
例子:
(1)如果不用虚继承,用多继承:
#include <iostream>
using namespace std;
class Grandfather
{
public:
Grandfather()
{
cout << "Grandfather constructor called" << endl;
}
protected:
int a = 0;
void fun() {}
};
class FatherA : public Grandfather
{
public:
FatherA()
{
cout << "FatherA constructor called" << endl;
}
void show()
{
cout << "FatherA" << endl;
}
};
class FatherB : public Grandfather
{
public:
FatherB()
{
cout << "FatherB constructor called" << endl;
}
void show()
{
cout << "FatherB" << endl;
}
};
class Son : public FatherA, public FatherB
{
public:
Son()
{
cout << "Son constructor called" << endl;
}
void show()
{
cout << "Son" << endl;
}
};
int main()
{
// Grandfather grandfather;
// FatherA fatherA;
// FatherB fatherB;
Son son;
cout << "sizeof(Son)=" << sizeof(son) << endl;
// cout << "sizeof(Grandfather)=" << sizeof(grandfather) << endl;
// cout << "sizeof(fatherA)=" << sizeof(fatherA) << endl;
// cout << "sizeof(fatherB)=" << sizeof(fatherB) << endl;
return 0;
}
/*
Grandfather constructor called //间接基类构造函数调用了两次
FatherA constructor called
Grandfather constructor called
FatherB constructor called
Son constructor called
sizeof(Son)=8 //Son类中继承了两个间接基类的数据成员
*/
(2)使用虚继承#include
class Grandfather
{
public:
Grandfather()
{
cout << "Grandfather constructor called" << endl;
}
protected:
int a = 0;
void fun() {}
};
class FatherA : virtual public Grandfather
{
public:
FatherA()
{
cout << "FatherA constructor called" << endl;
}
void show()
{
cout << "FatherA" << endl;
}
};
class FatherB : virtual public Grandfather //虚继承
{
public:
FatherB()
{
cout << "FatherB constructor called" << endl;
}
void show()
{
cout << "FatherB" << endl;
}
};
class Son : public FatherA, public FatherB //虚继承
{
public:
Son()
{
cout << "Son constructor called" << endl;
}
void show()
{
cout << "Son" << endl;
}
};
int main()
{
// Grandfather grandfather;
// FatherA fatherA;
// FatherB fatherB;
Son son;
cout << "sizeof(Son)=" << sizeof(son) << endl;
// cout << "sizeof(Grandfather)=" << sizeof(grandfather) << endl;
// cout << "sizeof(fatherA)=" << sizeof(fatherA) << endl;
// cout << "sizeof(fatherB)=" << sizeof(fatherB) << endl;
return 0;
}
/*
Grandfather constructor called //间接基类构造函数只调用了一次
FatherA constructor called
FatherB constructor called
Son constructor called
sizeof(Son)=24 //注意这里Song的内存大小,(间接基类的int num占4,两个直接基类的两个虚函数表占8+8=16,然后根据字节对齐规则,总内存20扩大到8的倍数也就是24
*/
多态
虚函数
虚函数的使用方法:
(1)在基类用virtual声明成员函数为虚函数
(2)在派生类中重新定义此函数,要求函数名、函数的参数个数和类型全部与基类的虚函数相同,并根据派生类的需要重新定义函数体。
(3)定义一个指向基类对象的指针变量,并使它指向同一类族中的某一对象。
(4)通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数。 c++规定,当一个成员函数被声明为虚函数后,其派生类中原型相同的函数都自动成为虚函数 。派生类没有对基类的虚函数重新定义,则派生类继承其直接基类的虚函数。
类型转换:
static_cast:静态转型,必须是相关类型,非多态类层次的祖孙互易,void*转换任何类型
dynamic_cast:动态转型,专门针对有虚函数的继承结构,将基类指针或引用转换成想要的子类指针或引用
const_cast:常量转型,去掉常量性的转换。
使用:
dynamic_cast<Derived*>(p);
static_cast<Car*>(p);
const char* max(const char*,const char*);
char* p=const_cast<char*>(max(“hello”,”good”));
虚函数的实用意义:从基类继承来的某些成员函数不完全适应派生类的需要,允许其派生类中对该函数重新定义,赋予它新的功能,当基类的这些成员函数声明为虚函数后,可以通过指向其基类的指针指向同一类族中不同类的对象,从而调用其同名的函数。
由虚函数实现的多态性是:同一类族中不同类的对象,对同一函数调用作出不同的响应。
多态的实现:
联编(编联、束定、绑定)(binding):就是把一个标识符名和一个存储地址联系在一起的过程。将一个函数调用链接上相应于函数体的代码,这一过程就是函数联编。
静态联编:出现在运行前的联编(在编译时完成),也称为早期联编。
动态联编:联编工作在程序运行阶段完成的情况。在编译、连接过程中无法解决的联编问题,要等到程序开始运行之后再来确定。 也称为滞后联编。
虚函数的工作机理:
Virtual出现则每个类增加一个虚函数表保存类的虚函数 。凡有虚函数的类均维护一个虚函数表 ,实例化每个对象中会增加一个指针指向虚函数表(对象大小会有变化).。虚函数调用时不需要确定对象类型,通过该指针即可找到所要链接函数
多态注意事项:
非成员、静态成员、内联函数不能是虚函数
构造函数、赋值运算符函数不能是虚函数
析构函数经常定义成虚函数 delete p;
多态实现深拷贝的例子:
class Student{
int move_tel;
public: int id;
virtual Student * n(){
return(new Student(*this));}
};
class Master :public Student{
string topic;
public: //方法略
Student * n(){
return(new Master(*this));}
};
class Phd:public Student{
string professor;
public: //方法略
Student * n(){
return(new Phd(*this)); } };
class DoubleMaster :public Student{
string SecondMajor;
public: //方法略
Student * n(){
return(new DoubleMaster (*this));}
};
class Member{
Student * m[128];
public:
Member( ){ }
Member(Member &s){
for(int i=0;i<128;i++)
this->m[i] = s.m[i]->n();}
Student * &f(int k)
{return(m[k]);}
};
void main()
{Member s;
s.f(0)=new Student;
s.f(1)=new Master;
s.f(2)=new Phd;
s.f(3)=new DoubleMaster;
s.f(4)=new Phd;
Member t(s);
}
//定义了一个t,并完全拷贝s内容
抽象类
纯虚函数:在基类中只声明虚函数而不给出具体的函数定义体,称此虚函数为纯虚函数。
纯虚函数的声明如下: (注:要放在基类的定义体中)
virtual 函数原型=0;
声明了纯虚函数的类,称为抽象类(抽象基类)
通过该基类的指针或引用就可以调用所有派生类的虚函数,基类的纯虚函数只是用于继承,仅作为一个接口,具体功能在派生类中实现。
使用纯虚函数时应注意:
(1)抽象类中可以有多个纯虚函数。
(2)抽象类也可以定义其他非纯虚函数。
(3)从抽象类可以派生出具体或抽象类,但不能从具体类派生出抽象类。
问题:抽象类需要有构造函数和析构函数吗?
【答】虽然抽象类不能实例化,但是抽象类被继承之后,它的派生类可以实例化;而派生类在实例化调用构造函数的时候会先调用基类中的构造函数,所以抽象类的构造函数也是可以被调用的,所以抽象类中可以有构造函数。但是注意:C++核心准则C.126:抽象类通常不需要构造函数,因为抽象类通常不包含任何需要构造函数初始化的数据。
抽象类通常代表一个抽象的概念,它提供一个继承的出发点。 在一个复杂的类继承结构中,越上层的类抽象程度越高,有时甚至无法给出某些成员函数的实现,显然,抽象类是一种特殊的类,它一般处于类继承结构的较外层。 引入抽象类的目的,主要是为了能将相关类组织在一个类继承结构中,并通过抽象类来为这些相关类提供统一的操作接口,更好的发挥多态性。
类的六种关系
一、纵向关系:(耦合关系相同)
1、继承(is-a) 虎是一种动物
2、实现接口(is like a)(接口:是对行为的抽象) 飞机和鸟都会飞
二、横向关系:(耦合关系渐弱)
1、合成、组合(is a part of)(强拥有,严格的部分整体) 鸟和翅膀;生命周期同步。 形式:成员
2、聚合(own a)(弱拥有,A可以包含B,但B不是A的一部分) 大雁和雁群,群体和个体,生命周期不同步 形式:成员,一般为容器
3、关联(has a) 人有朋友,不是包含关系。企鹅和气候的关系 形式:成员
4、依赖(use a)(运行期关系) 动物需要呼吸氧气 形式:局部变量、方法的参数或者对静态方法的调用
其关系强弱(耦合度)为 依赖<关联<聚合<组合<继承<实现<继承
能使用组合或聚合就不要使用继承(降低耦合度)
关联和聚合的区别:主要在语义上,关联的两个对象之间一般是平等的,例如你是我的朋友,聚合则一般不是平等的,例如一个公司包含了很多员工,其实现上是差不多的。聚合和组合的区别则在语义和实现上都有差别,组合的两个对象之间其生命期有很大的关联,被组合的对象是在组合对象创建的同时或者创建之后创建,在组合对象销毁之前销毁。一般来说被组合对象不能脱离组合对象独立存在,而且也只能属于一个组合对象,例如一个文档的版本,必须依赖于文档的存在,也只能属于一个文档。聚合则不一样,被聚合的对象可以属于多个聚合对象,例如一个员工可能可以属于多个公司。
我想举个通俗的例子。
你和你的心脏之间是composition组合关系(心脏只属于自己)
你和你买的书之间是aggregation聚合关系(书可能是别人的)
你和你的朋友之间是association关联关系
组合和聚合的区别:
聚合关系(Aggregation)体现的是A对象可以包含B对象,但B对象不是A对象的组成部分。具体表现为,如果A由B聚合成,表现为A包含有B的全局对象,但是B对象可以不在A创建的时刻创建。
组合关系(Composition):如果A由B组成,表现为A包含有B的全局对象,并且B对象在A创建的时刻创建。
现实生活中,人和人和手,脚是组合关系,因为当人死亡后人的手也就不复存在了。人和他的电脑是聚合关系
组合实例:
public class Person {
private Hand hand;
public Person() {
hand=new Hand();
}
private void go(){
hand.hand();
}
public static void main(String[] args) {
new Person().go(); //person消亡时,hand也消亡
}
}
class Hand{
public void hand(){
System.out.print("hand");
}
}
聚合实例:
public class Person2 {
private Computer c;
public Person2(){
}
public Person2(Computer c){
this.c=c;
}
public void go(){
c.computer();
}
public static void main(String[] args) {
Computer computer=new Computer();
Person2 person = new Person2(computer);
person.go(); //person消亡时,不影响computer
}
}
class Computer{
public void computer(){
System.out.print("computer");
}
}
设计原则
“高内聚低耦合”
1、单一职责原则
2、开放封闭原则
3、依赖倒转原则
4、里氏替换原则
5、合成聚合复用原则
6、迪米特法则(最少知识原则)
7、接口隔离原则
终结类
前提:不通过关键词final
思路:构造函数私有则该类不能用来继承,则为终结类
方法一:静态成员方法
#include <iostream>
using namespace std;
class AA{
private:
AA() {}
public:
static AA *GetAAObj() { return new AA; }
//static AA GetAAObj(){ return AA();}
static void DeleteAAObj(AA *pa) { delete pa; }
void show(){
cout << "hehe" << endl;
}
protected:
int _aa;
};
class BB : public AA{
};
int main(){
AA::GetAAObj()->show();
//BB b;错误:无法引用 "BB" 的默认构造函数 -- 它是已删除的函数
//note: 'BB::BB()' is implicitly deleted because the default definition would be ill-formed:
// error: 'AA::AA()' is private within this context
return 0;
}
方法二:虚拟继承
(1)典型方法
把基类的构造函数先设为protected,然后在派生类中变为private,使基类不可在被继承
虚拟继承的核心:一个基类如果被虚拟继承,那么在创建它的孙子类的对象时,该基类的构造函数需要单独被调用。此时,如果该基类的构造函数在孙子类的构造函数中无法访问,那么就实现了基类的子类不能被继承。基类 FinalParent,它不定义任何数据成员,这样任何类从它派生并不会增加任何空间上的开销。将它的默认构造函数的访问权限设定为 protected,这样它自身不能产生任何实例,只能用作基类。
当 FinalClassChild 试图继承 FinalClass 的时候,FinalClassChild 的构造函数中需要调用 FinalParent 的构造函数,而 FinalParent 的构造函数在 FinalClass 中已经变成了私有 private,不能被 FinalClassChild 的任何成员函数所访问,导致编译错误。所以,任何一个类,只要虚拟继承类 FinalParent,就不能被继承,从而简单、高效、安全地实现了终结类。
#include <iostream>
using namespace std;
class FinalParent{
protected:
FinalParent() {}
};
class FinalClass :private virtual FinalParent{
public:
FinalClass() {}
//其他略,类要实现的功能或者定义的数据成员可以写在这里
};
class A :public FinalClass{};
int main(){
FinalClass f;
A a; //错
return 0;
}
//[Note] 'A::A()' is implicitly deleted because the default definition would be ill-formed:
//[Error] 'FinalParent::FinalParent()' is protected
(2)
把基类构造函数设为private,派生类设为友元类,可以调用基类构造,而派生类的派生类无法调用基类构造函数
#include <iostream>
using namespace std;
class Vb {private:
Vb() {} //构造函数私有
public:
friend class Student; //Student为友元类,可以调用基类构造函数
void show(){
cout<<"hello"<<endl;
}
};
class Student :virtual public Vb {
public:
Student(){}
};
class A :public Student {}; //A无法调用间接基类的构造函数
int main() {
Student s;
s.show();
A a; //错
return 0;
}
final和override关键字
1、类被final修饰,不能被继承
class A1 final { };
class B1 : A1 { }; // “B1”: 无法从“A1”继承,因为它已被声明为“final”
2、虚函数被final修饰,不能被override
class A1{
virtual void func() final {} };
class B1 : A1{
virtual void func() {}
//“A1::func”: 声明为“final”的函数无法被“B1::func”重写
};
3、被override修饰后如果父类无对应的虚函数则报错。override就是编译器辅助你检查是否继承了想要虚继承的函数
struct A1{ virtual void func(int) {}};
struct B1 : A1{ virtual void func(int) override {} // OK
virtual void func(double) override {} //错
// “B1::func”: 包含重写说明符“override”的方法没有重写任何基类方法
};
模板
定义函数模板使用保留字template,
定义格式如下:
template < 模板参数表 >
返回类型 函数名( 函数形式参数表 ) { ... }
模板参数表中的参数可以有多个,可以是类型形参,也可以是表达式形参。多个参数间用逗号间隔。
模板参数若是代表一个类型,模板类型参数形式如下:
class 类型参数名 (或 typename 类型参数名)
函数模板的定义可以看作由两部分组成, 一是模板类型的定义,template<class 模板形参表>。 二是函数定义,它与普通函数的定义类似。 函数模板只是对函数的描述,编译系统不为其产生任何执行代码。
template <class T> //class可以用typename代替
void swap1(T& a,T& b){
T temp=a;
a=b;
b=temp;
}//函数模板
swap1<int>(a,b);//显式
swap1(a,b);//隐式
C++ 编译器在遇到调用用模板方式定义的函数时,会根据调用函数的参数类型构造出一个个具体的函数。这个过程称为函数模板的实例化(instantiation)。
同一个模板生成的不同的模板函数是不同名字的函数,不是重载函数。函数模板反映的是不同函数的函数族。使用模板实现了代码重用。
模板参数表中的参数可以有多个,多个参数间用逗号间隔。参数值可默认
template<typename T,typename U> //模板类型声明不能共享;typename不能节省template<classname T,U> 错误
void add(T a,U b)
{ cout<<a+b<<endl;}
void main(){
int x=1,y=2;
float s=3.0,t=4.0;
add(x,y);
add(s,t);
add(x,t);
add(s,y);//以上四条语句均正确;因为T和U
//可以相同也可以不相同
add<float,int>(s,y);
add<float>(s,y);
add<,int>(s,y);//错误程序员可以仅提供部分模板实参,其余的模板实参仍由编译器自动推导。且省略掉的实参必须是模板参数表尾部的参数对应的实参。
}
优先匹配非模板函数
#include <iostream>
using namespace std;
template <class T>
void swap(T &a, T &b){
T tmp = a;
a = b;
b = tmp;
cout << "template function called" << endl;
}
void swap(int &a, int &b){
int tmp = a;
a = b;
b = tmp;
cout << "non-template function called" << endl;
}
int main(){
int a = 1, b = 2;
swap(a, b);
cout << "a=" << a << " b=" << b;
return 0;
}
//non-template function called
//a=2 b=1
模板的参数是非类型参数:
模板非类型参数:则形参的类型是某种具体的数据类型。
模板非类型参数表示该参数名代表了一个潜在的值,而该值代表了模板定义中的一个常量。
模板非类型参数被用作一个常量值出现在模板定义的余下部分。 它可以用在要求常量的地方,如 数组声明中指定数组的大小或作为枚举常量的初始值.
template <class T, int i>
int find(T a[],T k)
{
for(int j=0;j<i;j++)
if(a[j]==k)
return(j);
return(-1);
}
void main(){
int a[10]={0};
cout<<find<int,10>(a,1);
}
类 模板
template <typename T=int>//类模板也允许默认参数
class Stack{
public:
void push( T );
T pop( );
private:
T *head;
};
//与普通类一样,类模板的成员函数也可以在类模板定义中定义具体实现,这时,该函数是内联的(inline)。
类模板可以有多个类型参数
template <typename T,typename U> //多个类型参数
class List{… …};
List <NODE,Student> k;
I/O流
C++语言系统为实现数据的输入和输出定义了一个庞大的类库,它包括的类主要有:
ios:抽象基类
iostream:输入流类istream,输出流类ostream,输入输出流类iostream;
对标准输入设备和标准输出设备的输入输出,简称为标准I/O流。
fstream:输入文件流类ifstream,输出文件流类ofstream,输入输出文件流类fstream;
对在外存磁盘上文件的输入输出,简称为文件I/O流。
Strstream:输入字符串流类istrstream,输出字符串流类ostrstream,输入输出字符串流类strstream.
sstream:输入字符串流类istringstream,输出字符串流类ostringstream,输入输出字符串流类stringstream.
对内存中指定的空间进行输入输出。通常指定字符数组、string类对象做为存储空间的输入输出,简称为串I/O流。
异常处理
(1)框定异常(try语句块) 将那些有可能产生错误的语句放在try块中
(2)抛掷异常(throw语句) 检测是否产生异常,若是,则抛掷异常。
(3)捕捉异常,定义异常处理(catch语句块)将异常处理语句放在catch块中,以便异常被传递过来时就处理它。
C++只理会受监控的异常。 Try块之后必须紧跟一个或多个catch语句。 Catch括号中只能容纳一个形参,与throw抛掷的异常类型匹配时,便捕获了异常。 避免把正常逻辑淹没在错误处理代码中,从而使程序更易于阅读。
异常捕捉的类型匹配之苛刻程度可以和模板的类型匹配媲美,它不允许相容类型的隐式转换。
对于没有捕捉到的异常,Abort()进程被调用,从而无条件的中止程序的执行。
异常也可以抛出类:
class A{
char net[20];
public:
virtual void x(){cout<<"网络错误";}
};
class B:public A{
long card;
public:
void x(){cout<<"网卡错误";}
};
class C:public B{
int port;
public:
void x(){cout<<"端口错误";}
};
void net() //void net() throw(A,B,C)
{ throw B();}
--------------------------------------------------
void net() throw(A,B,C); //异常申述(异常说明) 声明和定义要一致
void main()
{ try{net();}
catch(A & s){s.x();} }
如果在函数的声明中没有包括异常接口声明,则此函数可以抛掷任何类型的异常,例如: void net( );
一个不抛掷任何类型异常的函数可以进行如下形式的声明: void net( ) throw();