c/cpp学习笔记
基础知识回顾
代码书写应该规范,注释简洁合理。函数变量命名要符合规则。总之要能看懂。
常量定义
#define 宏常量
const 修饰变量
#define weekday 7 //最前面,全局
const int monthday = 30;
创建.h后缀名的头文件
创建.cpp后缀名的源文件
在头文件中写函数声明
在源文件中写函数定义
(源文件要包含头文件,#include filename.h
)
指针
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int *p;
p = &a;
cout << "a的地址为"<< &a << endl;
cout << "a="<< a << endl;
cout << "指针p为" << p << endl;
cout << "*p=" << *p << endl;
*p = 100;
cout << "a="<< a << endl;
cout << "指针p为" << p << endl;
cout << "*p=" << *p << endl;
return 0;
}
a的地址为0x5ffe94
a=10
指针p为0x5ffe94
*p=10
a=100
指针p为0x5ffe94
*p=100
什么指针常量、指针函数、函数指针主要看后面一个词,前面是修饰的定语,代表属性
对象的初始化和清理
构造函数和析构函数
对象的初始化和清理工作编译器会强制执行。
如果我们不提供构造和析构函数,编译器会自动提供
编译器提供的构造函数和析构函数是空实现。
构造函数语法:类名(){}
- 构造函数,没有返回值也不写void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
析构函数语法: ~类名(){}
- 析构函数,没有返回值也不写void
- 函数名称与类名相同,在名称前加上符号 ~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
构造函数调用规则
Person() { } // 无参构造
Person(int a) { } //有参构造函数
Person(const Person& p) { }//拷贝构造函数
~Person() { }//析构函数
默认情况下,c++编译器至少给一个类添加3个函数
1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认拷贝构造函数,对属性进行值拷贝
//C++中利用new操作符在堆区开辟数据
//利用new创建的数据,会返回该数据对应的类型的指针
//深拷贝与浅拷贝
Person(const Person& p) {
cout << "拷贝构造函数!" << endl;//拷贝构造函数
//如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
m_age = p.m_age;
m_height = new int(*p.m_height);
}
初始化
初始化列表,用来初始化属性
语法:构造函数():属性1(值1),属性2(值2)... {}
//当类中成员是其他类对象时,我们称该成员为 对象成员
//构造的顺序是:先调用对象成员的构造,再调用本类构造
//析构顺序与构造相反
静态成员
- 静态成员变量
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
- 静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
C++对象模型和this指针
this指针指向被调用的成员函数所属的对象
this指针的用途:
- 当形参和成员变量同名时,可用this指针来区分(防止命名冲突)
- 在类的非静态成员函数中返回对象本身,可使用return *this
友元
友元的目的就是让一个函数或者类 访问另一个类中私有成员
友元的关键字为 friend
友元的三种实现
- 全局函数做友元(将全局函数放进类里,给予friend权限
friend void function
) - 类做友元 (将其他类放进类里,给予friend权限
friend class 类名
) - 成员函数做友元(将其他类下的成员函数放进类里,给予friend权限
friend void function::类名()
)
运算符重载
运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型
也可以用函数重载实现。
- 加号运算符重载:实现两个自定义数据类型相加的运算
- 左移运算符重载:输出自定义数据类型
- 递增运算符重载:实现自己的整型数据
- 赋值运算符重载
- 关系运算符重载:让两个自定义类型对象进行对比操作
- 函数调用运算符重载:称为仿函数,可以灵活使用
面向对象
封装的意义
封装是C++面向对象三大特性之一
语法: class 类名{ 访问权限: 属性 / 行为 };
封装的意义:
- 将属性和行为作为一个整体,表现生活中的事物
- 将属性和行为加以权限控制
封装意义一: 在设计类的时候,属性和行为写在一起,表现事物
封装意义二: 类在设计时,可以把属性和行为放在不同的权限下,加以控制
访问权限有三种:
- public 公共权限
- protected 保护权限
- private 私有权限
继承 Inheritance
继承是面向对象三大特性之一
类中,下级别的成员除了拥有上一级的共性,还有自己的特性。
利用继承的技术,减少重复代码
class A : 继承方式 B;
A 类称为子类 或 派生类
B 类称为父类 或 基类
继承方式一共有三种:
* 公共继承 public
* 保护继承 protected
* 私有继承 private
访问父类同名成员 需要加作用域
利用虚继承解决菱形继承的问题 class A : virtual 继承方式 B;
多态 polymorphic
多态是C++面向对象三大特性之一
多态分为两类
- 静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
- 动态多态: 派生类和虚函数实现运行时多态
静态多态和动态多态区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
利用虚函数实现晚绑定
//多态满足条件:
//1、有继承关系
//2、子类重写父类中的虚函数
//多态使用:
//父类指针或引用指向子类对象
//重写:函数返回值类型 函数名 参数列表 完全一致称为重写
//多态优点:代码组织结构清晰,可读性强,利于前期和后期的扩展以及维护
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此可以将虚函数改为纯虚函数
纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;
当类中有了纯虚函数,这个类也称为抽象类
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此可以将虚函数改为纯虚函数
纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;
当类中有了纯虚函数,这个类也称为抽象类
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构区别: 如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法:virtual ~类名(){}
纯虚析构语法: virtual ~类名() = 0;
类名::~类名(){}
函数模板
-
C++另一种编程思想称为 泛型编程 ,主要利用的技术就是模板
-
C++提供两种模板机制:函数模板和类模板
语法:
template<typename T>
函数声明或定义
//具体化,显示具体化的原型和定意思以template<>开头,并通过名称来指出类型
//具体化优先于常规模板
//普通函数模板
template<class T>
bool myCompare(T& a, T& b)
template<> bool myCompare(Person &p1, Person &p2) //具体化
语法:
template<typename T>
类
当类模板碰到继承时,需要注意一下几点:
- 当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型
- 如果不指定,编译器无法给子类分配内存
- 如果想灵活指定出父类中T的类型,子类也需变为类模板
问题:类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到
解决:
- 解决方式1:直接包含.cpp源文件
- 解决方式2:将声明和实现写到同一个文件中,并更改后缀名为.hpp,hpp是约定的名称,并不是强制
STL
- STL(Standard Template Library,标准模板库)
- STL 从广义上分为: 容器(container) 算法(algorithm) 迭代器(iterator)
- 容器和算法之间通过迭代器进行无缝连接。
- STL 几乎所有的代码都采用了模板类或者模板函数
STL大体分为六大组件,分别是:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器
- 容器:各种数据结构,如vector、list、deque、set、map等,用来存放数据。
- 算法:各种常用的算法,如sort、find、copy、for_each等
- 迭代器:扮演了容器与算法之间的胶合剂。
- 仿函数:行为类似函数,可作为算法的某种策略。
- 适配器:一种用来修饰容器或者仿函数或迭代器接口的东西。
- 空间配置器:负责空间的配置与管理。
容器
vector
动态数组,在超出内存时会自动重新分配内存。还可以自定义数据类型,使用起来很方便。
vector容器可以嵌套使用,父容器不改变子容器内存,父容器存储子容器副本或指向子容器的指针。
编译器为了避免频繁清空内存分配内存操作,所以往往会预分配一定的容量给vector。
vector<int> v1; //无参构造
vector<T> v; //采用模板实现类实现,默认构造函数
vector(v.begin(), v.end()); //将v[begin(), end())区间中的元素拷贝给本身。
vector(n, elem); //构造函数将n个elem拷贝给本身。
vector(const vector &vec); //拷贝构造函数。
vec1.insert(vec1.end(),5,3); //从vec1.back位置插入5个值为3的元素
vec1.pop_back(); //删除末尾元素
vec1.push_back(n); //增添末尾元素n
vec1.erase(vec1.begin(),vec1.end());//删除之间的元素,其他元素前移
vec1.clear(); //清空元素
cout<<(vec1==vec2)?true:false; //判断是否相等==、!=、>=、<=...
vector<int>::iterator iter = vec1.begin(); //获取迭代器首地址
vector<int>::const_iterator c_iter = vec1.begin(); //获取const类型迭代器
for (vector<int>::iterator it = vec.begin(); it != vec.end(); it++) {} //循环遍历
int size = vec1.size(); //元素个数
bool isEmpty = vec1.empty(); //判断是否为空
capacity(); //容器的容量
resize(int num); //重新指定容器的长度为num,若容器变长,则以默认值填充新位置。
//如果容器变短,则末尾超出容器长度的元素被删除。
resize(int num, elem); //重新指定容器的长度为num,若容器变长,则以elem值填充新位置。
//如果容器变短,则末尾超出容器长度的元素被删除
vec1.swap(vec2) //将vec1和vec2元素互换----->可以解决resize带来的内存浪费-->vec1.swap(vec1)
reserve(int len); //容器预留len个元素长度,预留位置不初始化,元素不可访问。
deque
deque,从前后两端都可以进行数据的插入和删除操作,同时支持数据的快速随机访问。
类似vector。支持随机访问
deque与vector区别:
- vector对于头部的插入删除效率低,数据量越大,效率越低
- deque相对而言,对头部的插入删除速度回比vector快
- vector访问元素时的速度会比deque快,这和两者内部实现有关
- deque没有容量的概念!deque是由多个固定大小的块组成的,这些块不一定是连续的。
deque<int> d1; //无参构造
deque<T> d; //采用模板实现类实现,默认构造函数
deque(v.begin(), v.end()); //将v[begin(), end())区间中的元素拷贝给本身。
deque(n, elem); //构造函数将n个elem拷贝给本身。
deque(const deque &deq); //拷贝构造函数。
deq1.insert(deq1.end(),5,3); //从vec1.back位置插入5个值为3的元素
deq1.pop_back(); //删除末尾元素
deq1.push_back(n); //增添末尾元素n
deq1.push_front(n); //增添头部元素n
deq1.pop_front(); //删除头部元素
deq1.erase(deq1.begin(),deq1.end());//删除之间的元素,其他元素前移
deq1.clear(); //清空元素
cout<<(deq1==deq2)?true:false; //判断是否相等==、!=、>=、<=...
deque<int>::iterator iter = deq1.begin(); //获取迭代器首地址
deque<int>::const_iterator c_iter = deq1.begin(); //获取const类型迭代器
for (deque<int>::iterator it = deq.begin(); it != deq.end(); it++) {} //循环遍历
int size = deq1.size(); //元素个数
bool isEmpty = deq1.empty(); //判断是否为空
resize(int num); //重新指定容器的长度为num,若容器变长,则以默认值填充新位置。
//如果容器变短,则末尾超出容器长度的元素被删除。
resize(int num, elem); //重新指定容器的长度为num,若容器变长,则以elem值填充新位置。
//如果容器变短,则末尾超出容器长度的元素被删除
vec1.swap(vec2) //将vec1和vec2元素互换----->可以解决resize带来的内存浪费-->vec1.swap(vec1)
reserve(int len); //容器预留len个元素长度,预留位置不初始化,元素不可访问。
sort(iterator beg, iterator end) //对beg和end区间内元素进行排序
list
STL中的链表是一个双向循环链表
功能: 将数据进行链式存储
链表(list)是一种物理存储单元上非连续的存储结构,数据元素的逻辑顺序是通过链表中的指针链接实现的
链表的组成:链表由一系列结点组成
结点的组成:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域
由于链表的存储方式并不是连续的内存空间,因此链表list中的迭代器只支持前移和后移,属于双向迭代器
list的优点:
- 采用动态存储分配,不会造成内存浪费和溢出
- 链表执行插入和删除操作十分方便,修改指针即可,不需要移动大量元素
list的缺点:
- 链表灵活,但是空间(指针域) 和 时间(遍历)额外耗费较大
List有一个重要的性质,插入操作和删除操作都不会造成原有list迭代器的失效,这在vector是不成立的。
list语法基本同vector
- insert(pos,elem);//在pos位置插elem元素的拷贝,返回新数据的位置。
- insert(pos,n,elem);//在pos位置插入n个elem数据,无返回值。
- insert(pos,beg,end);//在pos位置插入[beg,end)区间的数据,无返回值。
list容器中不可以通过[]或者at方式访问数据 reverse();
//反转链表sort();
//链表排序
存储方式 | 插入和删除的时间复杂度 | 访问特定位置元素的时间复杂度 | |
---|---|---|---|
list | 使用双向链表实现,内存不连续 | 任意位置操作时间复杂度为 O(1) | O(n),因为需要从链表头或尾开始遍历 |
deque | 双端队列,使用多个固定大小的块来存储元素 | 两端时间复杂度为 O(1)。在中间位置操作则可能较慢 | 接近 O(1),不如 vector 高效 |
vector | 动态数组,元素在内存中连续存储 | 尾部操作时间复杂度为 O(1),中间或开始位置操作时间复杂度为 O(n) | 访问特定位置元素的时间复杂度为 O(1) |
set
所有元素都会在插入时自动被排序(默认从小到大,可自定义,对于自定义数据类型,set必须指定排序规则才可以插入数据)
set/multiset属于关联式容器,底层结构是用二叉树实现。
- set不允许容器中有重复的元素
- multiset允许容器中有重复的元素
插入删除操作时仅仅移动指针即可,不涉及内存的移动和拷贝
map
pair对组,成对出现的数据,利用对组可以返回两个数据
pair<type, type> p ( value1, value2 );
pair<type, type> p = make_pair( value1, value2 );
map/multimap属于关联式容器,底层结构是用二叉树实现。
- map中所有元素都是pair
- pair中第一个元素为key(键值),起到索引作用,第二个元素为value(实值)
- map会按照key的值内部进行排序,但是保持其键值对的对应关系不变。
- 可以根据key值快速找到value值
- map不允许容器中有重复key值元素
- multimap允许容器中有重复key值元素
map保存的是键值对,所以前面的int类型数据(key)并不代表其位置。比方说,我们将其中的int修改为float也是可以的。
//第一种:用insert函数插入pair数据:
map<int, string> my_map;
my_map.insert(pair<int, string>(1, "first"));
//第二种:用insert函数插入value_type数据:
my_map.insert(map<int, string>::value_type(3, "third"));
//第三种:用数组的方式直接赋值:
my_map[5] = "fifth";
string
string是C++风格的字符串,而string本质上是一个类
char * 是一个指针
string是一个类,类内部封装了char*,管理这个字符串,是一个char*型的容器
stack
先进后出,栈中只有顶端的元素才可以被外界使用,因此栈不允许有遍历行为
构造函数:
stack<T> stk; //stack采用模板类实现, stack对象的默认构造形式
stack(const stack &stk); //拷贝构造函数
赋值操作:
stack& operator=(const stack &stk); //重载等号操作符
数据存取:
push(elem); //向栈顶添加元素
pop(); //从栈顶移除第一个元素
top(); //返回栈顶元素
大小操作:
empty(); //判断堆栈是否为空
size(); //返回栈的大小
queue
先进先出
队列不提供迭代器,更不支持随机访问
- 入队 — push
- 出队 — pop
- 返回队头元素 — front
- 返回队尾元素 — back
- 判断队是否为空 — empty
- 返回队列大小 — size