C++语法笔记
本文最后更新于:2022年5月27日 晚上
C++语法学习记录,做一个系统性梳理。
C++学习路线
C++语言学习
面向对象编程思想;
类的封装,构造和析构、静态成员、对象管理;
类的构造(有参构造函数、无参构造、拷贝构造、默认构造函数)和析构;
对象动态管理、友元函数、友元类、操作符重载;
C++编译器对象管理模型分析;
类对象的动态管理(new/delete);
友元函数和友元类;
运算符重载(一元运算符、二元运算符、运算符重载难点、项目开发中的运算符重载);
类的继承、多继承及其二义性、虚继承;
多态(概念、意义、原理剖析、多态案例);
虚函数、纯虚函数、抽象类(面向抽象类编程思想案例);
函数模板、类模板,模板的继承;
C++类型转换;
C++输入输出流(标准I/O文件I/O 字符流I/O);
C++异常处理(异常机制、异常类型、异常变量、异常层次结构、标准异常库)
C++开发
- STL
- 设计模式
- 数据结构基础
顺序存储、链式存储、循环链表;
双向链表、栈(顺序和链式)、队列(顺序和链式);
栈的应用、树基本概念及遍历、二叉树;
排序算法、并归算法、选择、插入、快速、希尔 - UI界面开发
- Unix/Linux网络服务器
- 数据库开发
C++基础语法
C++相对C语言的特性
- C语言:程序化开发语言,面向过程思想,适用于小规模问题的程序
- C++:面向对象的编程思想,适用于大规模问题的合作开发
C++的加强部分:
- 命名空间
- 重载:函数重载、运算符重载
- 引用
- 面向对象:封装、继承、多态
- 泛型编程
- 异常处理
- 标准库STL
命名空间
- 为了解决同一个作用域的符号名冲突,划分空间,区分同名
1 |
|
- 命名空间取别名 —- 一个命名空间可取多个别名,名字使用无区别
1 |
|
- 匿名命名空间 —- 定义自己命名空间不取名字,可直接使用里面内容,但仅当前文件有效
1 |
|
C++的输入和输出
C++ 中的输入输出通过流的方式来实现
流运算符 —- 做了函数重载
输出:
<<
输入:
>>
定义
标准输入对象:
cout
标准输入对象:
cin
—- 输入类型不匹配返回NULL
, 消除了连续输入多次字符产生垃圾字符的缺点换行:
endl
1
2
3
4
5
6
7
8
9cout.width(5); //设置域宽为5
cout << oct << 5; //用八进制显示
/*标志设置*/
oct //八进制
hex //十六进制
dec //十进制
showbase //显示前缀
C++的函数
带默认参数的函数
存在默认值的函数,即使不传参也会按默认参数运行
可在函数声明或实现的时候添加默认值,但是不能同时添加,建议声明时加默认值
默认值可全部设置、可全部不设置,但部分默认值的时候,必须把默认值参数放参数列表后面
1 |
|
带占位参数的函数
- 为了方便以后扩展功能,预留的参数,解决C中不规范函数传参
- 占位参数不适用,函数定义时候,只写类型,不写变量名
- 可加默认值,调用函数必须传入占位参数
1 |
|
函数重载
底层原理:编译器会将重载函数,设置为不同的函数名,根据参数类型顺序个数进行匹配
提高了函数的易用性
在同一作用域,一组函数名相同,参数列表不同的函数
重载通常是命名一组功能相似的函数,减少函数名的数量,提高程序的可读性
条件:
- 函数名必须相同
- 参数列表必须不同(个数、类型、顺序)
- 函数的返回值不能单独作为构成重载的条件,可以相同,可以不同
1 |
|
- const 关键字一般是不能构成重载的,但是const修饰的变量是引用时,可以构成重载
- const 修饰类的成员函数时,可以和非const成员函数构成重载
C++引用
原理、作用和使用规则
原理:引用的本质是,指针常量的使用:
int *const p
作用:
- 简化指针操作
- 引用给一个变量起了一个别名,对引用操作与对其绑定的变量或对象,操作一样
规则:
<类型> &<引用名> = <目标变量或对象名>
—-int & a = b
- 引用声明必须初始化,初始化后不能改变引用空间的位置 —- 必须绑定只能绑定一次
- 引用的类型和目标变量或对象类型必须一致
- 不能把已经有的引用名作为其他变量或对象的名字或别名
- 使用引用时,编译器底层生成指针,对其自动 * 运算
引用的用法
函数传参 (主要)
1
2
3
4
5
6void swap(int &a, int &b) //交换两个数
{
int tmp = a;
a = b;
b = tmp;
}函数返回值 (返回变量须静态,函数可做左值)
1
2
3
4
5
6
7
8
9
10
11
12
13
14int &retFunc(int a)
{
static b = a + 1;
return b;
}
int main()
{
int a = 1;
int &p = retFunc(a);
cout << p << endl;
retFunc(a) = 80;
cout << p << endl;
return 0;
}对数组引用
1
2
3
4
5
6
7
8int a[5] = {0};
int (&p)[5] = a;
int (&func())[5]
{
static int a[5] = {1,2,3,4,5};
return a;
}对指针引用 (几乎不用)
1
2
3int a = 10;
int *p = &a;
int *&q = p;
动态内存分配 —- new和delete
- new —- 申请内存并初始化对象
- delete —- 释放内存并销毁对象
1 |
|
C++动态内存管理—-new/delete | C语言动态内存管理—-malloc/free |
---|---|
C++ 操作符 | C/C++标准库函数 |
自己计算类型大小,返回对应类型指针 | 需要手动计算类型大小 返回void * |
调用构造函数和析构函数,初始化对象与销毁对象 | 只负责分配/释放空间 |
基于malloc/free实现的 | / |
C++面向对象之—-封装性
面向对象编程
面向过程 (Procedure Oriented —- PO)
解决问题时,面向过程会把事情拆分成,一个个函数和数据,按照顺序执行,完成任务
面向对象 (Object Oriented —- OO)
解决问题是,面向对象会把事情抽象成对象的概念。设计问题中的一个个对象,赋予他们属性和方法,让每个对象去执行自己的方法,解决问题
类和对象
类 —- 某一具体事物的抽象,用于描述某一类事物的一种数据类型,包括属性和方法(函数)
1
2
3
4
5
6class ClassName
{
Access specifiers : //访问权限 :访问修饰符
Data Members/variables; //数据成员
Member functions(); //成员函数
};1
2
3
4
5
6
7
8
9
10
11
12
13class Circle
{
public:
double calculateArea();
private:
int x;
int y;
int r;
};
double Circle::calculateArea()
{
return 3.14 * r * r;
}对象 —- 某一类事物的个体,具体且唯一,创造后才会分配空间
1
2
3
4<类名><对象名>; // Circle circle;
<类名> *<对象指针名> = new <类名>();
Circle * circle = new Circle();
delete circle; //记得删除,防止内存泄漏访问属性、成员函数
1
2
3
4
5
6
7
8/*普通对象*/
Circle circle;
circle.r = 10;
/*对象指针*/
Circle *circle = new Circle();
circle->r = 10;
delete circle;
circle = NULL;
构造函数 —- 不能定义为虚函数
1 |
|
默认构造函数
当类中没有构造函数,编译器自动生成一个构造函数
当自己写了构造函数,编译器便不会默认生成构造函数了
构造函数重载
针对不同的初始化方式,可以对构造函数进行重载
1 |
|
初始化列表
作用:方便传参、提高性能(针对对象成员)、继承的时候在子类构造函数传父类成员参数
只能在初始化列表的成员变量:
const 成员变量 — 被const修饰,已经成为常量,定义后不能被重新赋值
引用 — 引用定义同时必须初始化,且初始化后不能赋值
不含默认构造函数(默认参数)的类的对象
使用初始化列表,不调用默认构造函数初始化,而是调用拷贝构造初始化
构造函数的函数体中只能赋值,不能初始化,因此没有默认构造函数的类的对象,只有在初始化列表初始化
继承的时候,初始化父类
1 |
|
拷贝构造函数
调用拷贝构造函数的三种情形:
用一个对象初始化另一个同类对象的时候
用值传递的方式,给函数传对象参数的时候
用值传递的方式,让函数返回一个对象的时候
C++标准允许一种(编译器)实现省略创建一个只是为了初始化另一个同类型对象的临时对象。指定这个参数(-fno-elide-constructors)将关闭这种优化
优化方式是建立一个对象引用绑定到返回的优化,可以省略两次调用拷贝构造函数
深拷贝、浅拷贝
- 浅拷贝:默认拷贝为浅拷贝,针对指针对象,只拷贝指针存储的地址
- 深拷贝:针对指针对象,拷贝指针指向的空间
如果只写了拷贝构造函数,默认构造函数也不分配了,这时不能正常创建对象,类中此时只有一个构造函数
explicit
explicit关键字只能用来修饰类的构造函数,且最好只修饰只有一个参数的构造函数
被修饰的构造函数不能发生相应的隐式类型转换,只能以显示的方式进行类型转换
何时触发隐式拷贝构造函数?
一个对象作为函数参数,以值传递的方式传入函数体
一个对象作为函数返回值,以值传递的方式从函数返回
以A a = b的方式构造a,其中b也是A类型
所以在拷贝构造函数一般不会设计成禁止隐式转换
析构函数 —- 最好定义为虚函数
1 |
|
- 没有参数和返回值,但有this指针
- 析构函数不能使用const修饰
- 一个类有且只有一个析构函数,所以不能重载,但可以有多个构造函数
static(静态)关键字 (重点)
static
可以修饰成员变量与成员函数—— 静态成员访问 <类名>::<静态成员名>
什么时候使用
static
关键字- 设计类的构造函数时候,传参冲突,无法区分类型相同的不同参数时候,不能构成构造函数重载,可以通过static 函数解决这个问题
- 一切不需要实例化(创建对象),就可以有确定行为的函数都应该设计为静态的
使用方法:
- 静态成员变量一定要在类中定义,类外进行初始化 ,如果是多文件编程,静态成员变量的初始化写在类的.cpp文件中,不要在头文件中对静态变量初始化
this指针
用于保存对象的地址,this指针是隐藏在非静态成员中的
this指针只和对象相关,静态成员是没有this指针的
使用场景:
- 非静态函数中,返回对象本身
rentrun this
- 非静态函数中,区分传入的形参名和对象内成员变量名
this->x = x
- 非静态函数中,返回对象本身
const关键字 (重点)
const 修饰的成员变量,只能在初始化列表进行初始化,因为它已经是一个常量了,定义之后就不能赋值了
可以定义const常量,具有不可变性。
便于进行类型检查,使编译器对处理内容有更多了解,消除了一些隐患。
例如void f(const int i)
编译器就会知道i是一个常量,不允许修改;
可以避免意义模糊的数字出现,同样可以很方便地进行参数的调整和修改。 同宏定义一样,可以 做到不变则已,一变都变!如(1)中,如果想修改Max的内容,只需要:const int Max=you want;即可!
可以保护被修饰的东西,防止意外的修改,增强程序的健壮性。 还是上面的例子,如果在函数体内修改了i,编译器就会报错; 例如: void f(const int i)
为函数重载提供了一个参考。
1 |
|
- 可以节省空间,避免不必要的内存分配。
const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝。
- 提高了效率。
编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。
友元函数 —- friend
友元函数不是类的成员函数,在类中声明,在类外定义
友元函数可以访问所有私有成员和保护成员,一般不会使用,这样会破坏面向对象的封装性
注意:
- 友元不具有相互性 — A是B的友元,不代表B是A的友元
- 友元不能被继承 — 父类的友元不一定是子类的友元
- 友元不具备传递性 — 你的朋友的朋友不一定是你的朋友
运算符重载
运算符重载实际上对运算符赋予新的运算方式
目的是为了对象的运算操作简洁明了
规则:
大部分运算符可以重载,少数不行
不能被重载的运算符:
.
:成员访问运算符.*
,->*
: 成员指针访问运算符::
:域运算符sizeof
:长度运算符?:
:条件运算符#
:预处理符号
重载运算符可以对运算符号做出新解释,但基本语义不变
- 无法改变运算符优先级
- 无法改变运算符结构特性
- 无法改变运算符所需的操作数
- 无法创造新的运算符
语法:
- 类的成员函数:
<函数返回值> operator <运算符> (<形参表>) {}
- 类的友元函数:
<函数返回值> operator <运算符> (<形参表>) {}
- 类的成员函数:
补充
const与static的混淆点
(1)const 的变量只能通过构造函数的初始化列表进行初始化;(貌似在c++11中可以正常编译)
(2)static 的变量只能通过在类外重新定义进行初始化;
(3)static const 变量 只能通过在类中直接用”=”进行赋值
(4) const成员函数可以修改静态成员变量
(5)const成员函数不能访问非const成员变量,不能调用非const成员函数
(6)一般来说const不能单独构成重载,但是const修饰的变量若是引用,则可以构成重载
关于const的重载
重载要求同一个作用域函数名相同,形参表的本质不同
1 |
|
上面两个函数构成重载,本质在于:非const函数隐藏的this指针指向是正常类型的对象,const函数的this指针,指向了const类型的对象
从形参本质上说,这两个函数的形参表已经不同了
基于此,const int *a
和 int *a
也能构成重载。传进去的指针一个指向常量,一个指向变量,形参表也不相同了。
const int &a
和 int &a
也能构成重载,一个引用的是常量,一个引用的是变量
const int a
和 int a
不能构成重载,因为值传递,只是对形参赋值,和实参本体没有关系,加不加const
都没有分别
六个默认的成员函数
构造函数、拷贝构造函数、析构函数、赋值运算符重载、&运算符重载、const &运算符重载
C++面向对象之 —- 继承性
继承语法
1 |
|
继承类型
- 公有继承(public):基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员
- 保护继承(protected):基类的保护和公有成员都是派生类的保护成员
- 私有继承(private):基类的公有和保护成员都是派生类的私有成员
无法被继承的成员
基类的构造函数、拷贝构造函数、析构函数
基类的重载运算符
基类的友元函数
基类的私有成员
继承的函数隐藏 hiding
派生类类和基类有同名函数,总是调用派生类的函数,基类函数被隐藏
派生类继承的函数不适合派生类或需要拓展,则需要重写基类的函数
1 |
|
构造函数与析构函数的调用顺序
定义派生类的时候:
1.调用基类构造函数 : 继承多个基类的时候,调用顺序按照继承顺序 ,不是初始化顺序
2.成员对象的构造函数
3.派生类滋生的构造函数
析构函数 调用顺序和构造函数相反
多继承与多重继承
多继承:一个派生类继承多个基类
多重继承:一个基类的派生类,继续派生
避免菱形继承问题,这是一个有缺陷的设计方案
- 数据冗余:菱形底部的类实例化,会调用两次顶部类的构造函数
- 二义性问题
C++面向对象之 —- 多态性
C++中,一般针对一个行为只会有一个名称,是对类的行为在抽象,主要作用在于统一行为的接口,提高方法的通用性
两种多态:
- 静态多态:函数重载、泛型编程
- 动态多态:虚函数
静态绑定和动态绑定
定义:
- 动态绑定:运行时确定具体需要调用的函数
- 静态绑定:编译结束就确定了需要调用的函数
作用:
- 把不同的派生类对象都当作基类对象看待,屏蔽不同派生类对象的差异
- 提高程序的通用性适应需求的变化
使用方法:基类的指针或引用指向子类的对象
虚函数和动态多态
虚函数
- 使用
virtual
关键字声明的函数,是动态多态实现的基础 - 非类的成员函数不能声明为虚函数
- 类的静态成员函数不能为虚函数
- 构造函数不能定义为虚函数,析构函数最好定义为虚函数
- 基类的某个成员声明为虚函数后,派生类的同名函数自动成为虚函数
实现步骤
- 1.创建两个类,为继承关系
- 2.基类中函数声明为虚函数
- 3.派生类继承基类并重写基类虚函数
- 4.基类的 指针或引用 访问 基类或派生类对象
覆盖(重写)、重载、隐藏
重写:派生类重新实现基类的虚函数
不同作用域
函数名相同、参数相同、返回值相同
基类必须有虚函数
重写函数的权限限定符可以不同
函数重载:同一个作用域参数不同的同名函数
同一个作用域
函数名相同、参数不同、返回值可同可不同
可以不是虚函数
隐藏:基类和派生类同名函数,总是调用子类的函数,隐藏父类函数
不同作用域
函数名相同
参数不同,无论有没有virtual关键字,基类函数都将隐藏
参数相同,但基类没有virtual关键字,基类函数将被隐藏
虚析构函数
建议将基类的析构函数设置为是虚函数,基类的析构函数声明为虚函数,则派生类的析构函数自动为虚函数
当基类的指针指向派生类对象时,如果析构函数不是虚函数,则不会发生动态多态,而导致只会调用基类的析构函数,造成内存泄漏
虚函数原理和虚函数表
虚函数
c++ 能够在运行时确定调用的函数,是因为引入了虚函数。一旦类中引入了 虚函数,在程序编译期间, 就会创建虚函数表,表中每一项数据都是 虚函数的入口地址
为了将对象与虚函数表关联起来,编译器会在对象中会增加一个指针成员用于存储虚函数表的位置
基类的指针指向派生类对象时就是通过虚函数表的指针来找到实际应该调用的函数
虚函数表
基类与派生类都维护自己的虚函数表,如果派生类重写基类的虚函数,则虚函数表存储的是派生类的函数的地址,没有重写的虚函数则保存的是基类的虚函数表
抽象类和纯虚函数
定义
- 含纯虚函数的类,称为抽象类
- 纯虚函数:指定函数接口规范,而不做具体的实现,实现部分由继承它的子类去实现
特点和作用
- 抽象类中只声明函数接口,不能有具体的实现
- 抽象类不能创建对象,可以定义指针与引用
- 派生类继承基类,并且必须要实现基类中的所有纯虚函数,否则派生类也是抽象类
应用
- 基类只知道派生类需要的方法,不知道具体实现
- 多个具有相同特征的派生类中抽象一个类出来,作为派生类的模板,防止子类设计的随意性
C++ —- 泛型编程
泛型编程
- 不依赖具体的数据类型的程序
- 提高程序的通用性,将算法从数据结构中抽象出来,成为通用算法
模板
用不确定的类型参数产生一系列函数和类的机制
关键字:
template
工作方式:
函数模板
1 |
|
调用方式:—- 不允许隐式类型转换,调用类型必须严格匹配
- 自动类型推导
func(a);
- 具体类型显示调用
func<int>(a)
(推荐)
- 自动类型推导
原理:函数模板中声明了参数类型T,表示了一种抽象类型,编译器检查到程序调用函数的时候,根据传递参数的实际类型生成模板函数
和宏定义、函数对比:
宏定义:
- 优点:代码复用,适合所有的类型;
- 缺点:缺少类型检查,宏在预处理阶段被代替掉,编译器不知道宏的存在
函数:
- 优点:真正的函数调用,编译器对类型进行检查
- 缺点:类型不同时需要重复定义函数,代码无法复用
模板函数:
- 优点:代码复用,适合所有类型。克服普通函数弊端;编译器会进行类型检查。克服宏定义弊端
- 缺点:调试比较难,对程序员要求高;一般编写一个类型确定的函数,运行通过后,再修改成函数模板
类模板
- 对一批仅仅是成员数据类型不同的类的抽象
- 为一批类的家族,创建类模板,用以生成多种具体类
1 |
|
原理:
- 类模板实例化:类模板—>模板类
- 编译器自动用具体的数据类型替换类模板中的类型参数,生成模板类代码
二次编译机制:
编译时,编译器产生类的模板函数声明,实际确认类型后调用的时候,根据调用的类型,再次生成对应类型的函数声明和定义
二次编译机制,模板类中声明的友元函数,在类外实现时,找不到友元函数实现报错
- 类前置声明
- 友元模板函数的前置声明
- 友元模板函数增加泛型支持
补充:
模板的声明和实现要放在一个文件里,否则编译时找不到定义
如果非要用多文件分开写,就在头文件里把实现的源文件包括进来
C++ —- STL
- 六大组件:容器(Container)、迭代器(Iterator)、算法(Algorithm)、仿函数(Functor)、适配器(Adaptor)、分配器(Allocator)
容器
- 容纳、包含一组元素或元素集合
序列式容器
向量 — vector:本质是动态数组,尾端增删性能高
列表 — list:本质是双向循环链表,任何位置增删性能好,随机访问慢
双端队列 — deque:本质是动态数组,随机存取好,仅次于vector
关联式容器
- 查找数据性能好
集合(set)与多重集合(mulitiset):set不允许重复,mulitiset允许数据重复
映射(map)与多重映射(mulitimap):
- 键值对容器,数据成对出现,第一个值为key,第二个字为value,key只能在map出现一次,mulitimap允许key重复
迭代器
- 检查容器内元素并遍历元素的数据类型
- C++趋向用迭代器而不是下标
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!