一、C++语法
01、引用类型
引用类型是C++引入的新类型,根据汇编的知识进行理解,程序在汇编后,变量名将失去意义,因为汇编码将替换成用内存的(链接地址or运行地址)访问变量。在C/C++语言中,用变量名表示变量所占的那块内存,为了使多个名称绑定一个变量,C++引入了引用类型。其主要目的就是让变量名和变量能够真正的区分开,C语言中变量名和变量是1对1的,是分不开的。
int i = 1; //栈中分配sizeof大小内存,内存里的值为1,i <=> 内存的标号。
int &ri = i; //给变量取一个别名ri,ri <=> i。
注意:定义引用,并不是定义变量,因为定义引用是不开辟空间的。可以理解为定义变量名,所以它必须初始化,必须与变量绑定起来。
02、const限定符
const 对象必须初始化:
const int j = 42; //正确(编译时初始化)
const int i = get_size(); //正确(运行时初始化)
const int i; //错误
const 与普通类型的组合:
const int 与int const是等价的
const float 与 float const是等价的
。。。。
const 与指针类型组合(仅指针):
int c1 = 0;
const int c2 = 0;
const int *p1 = &c1; //左侧const限定右边的变量,即c1不能改变,限定右边的叫低层const
int * const p2 = &c2; //右侧const限定左边的变量,即p2不能改变,限定左边的叫顶层const
const int *const p3 = p2; //左右边的值都被限定,都不能改变
const 与引用的组合:
const int &ri = c2; //仅此一种写法,且只限定右边的变量
注意:const限定的都是变量,无论左边还是右边的变量都是左值,它并不针对右值。
注意:左值和右值不是根据在 = 号左的右来区分的(百度上有很多根据等于号这样的错误说法)
03、命名空间的写法
写法一:在cpp文件中,每个变量的定义都加上命名空间的名称。
写法二:在cpp文件中,每个变量的定义都用namespace框起来。
04、C++ STL中常用的数据结构
1、C++标准库(STL)之顺序型容器
vector:可变大小数组
deque:双端队列
list:双向链表
forward_list:单向链表
array:固定大小数组
string:与vector相似,但专门用于保存字符
=====>顺序容器适配器
stack:栈适配器,默认情况下基于dequeue实现
queue:队列适配器,默认情况下基于dequeue实现
priority_queue:优先队列适配器,默认情况下基于vector实现
2、C++标准库(STL)之关联性容器
=====>按关键字有序保存
map:关联数组
set:关键字即值,只保存关键字的容器
multimap:关键字可重复出现的map
multiset:关键字可重复出现的set
=====>关键字无序保存
unordered_map:用哈希函数组织的map
unordered_set:用哈希函数组织的set
unordered_multimap:哈希组织的map;关键字可以重复出现
unordered_multiset:哈希组织的set;关键字可以重复出现
总结:宏定义可以摆脱变量类型,但不具备类型的检查。模板不仅可以拜托变量类型,还可以进行类型的检查。泛型编程作用举例:函数重载时,函数逻辑相同,但形参类型不同,要写不同的函数,有了函数模板,只需要写一个函数就行了。
//vector使用示例========================================
int main()
{
std::vector<int> vec = { 1, 2, 3 };
vec.insert(vec.begin(), 11);
vec.push_back(99);
for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << std::endl;
}
std::cout << "------------------------" << std::endl;
for (auto& x : vec) {
std::cout << x << std::endl;
}
return 0;
}
//vector<int>::iterator it = vec.begin(); //对指向的内容可读可写
//vector<int>::const_iterator it = vec.begin(); //对指向的内容只能读
05、OOP的三大特征之类的封装
0、这里主要说明类的构造函数和初始化参数列表。
1、类的缺省构造函数
#include <iostream>
class Point{
public:
int x;
int y;
};
int main(int argc,char* argv[]){
Point pt; //调用缺省构造函数
//Point pt();
pt.x = 0;
pt.y = 1;
return 0;
}
==>说明:在不定义构造函数,会生成一个缺省构造函数,如下所示
class Point{
public:
Point(){
//一些默认初始化操作
};
int x;
int y;
};
2、一旦你定义构造函数,将不再生成缺省构造函数
#include <iostream>
class Point {
public:
Point(int j, int k) {
this->x = j;
this->y = k;
}
int x;
int y;
};
int main(int argc, char* argv[]) {
//Point pt; //错误,没有缺省构造函数
Point pt(5, 5); //正确
pt.x = 0;
pt.y = 1;
std::cout << pt.x << std::endl;
std::cout << pt.y << std::endl;
return 0;
}
=>说明:C++11允许我们使用=default来生成一个默认的构造函数
class Point {
public:
Point() = default;
Point(int j, int k) {
this->x = j;
this->y = k;
}
int x;
int y;
};
3、初始化参数列表
1、定义时初始化
class Point {
public:
Point() = default;
Point(int _x, int _y) : x(_x), y(_y) {
}
int x = 0;
int y = 0;
};
2、定义后初始化
class Point {
public:
Point() = default;
Point(int _x, int _y){
this->x = _x;
this->y = _y;
}
int x = 0;
int y = 0;
};
=>总结:初始化列表:
对类的成员进行:定义时初始化
初始化列表仅适用于构造函数(初始化列表是初始化成员变量的)
06、OOP的三大特征之类的继承
0、这里主要说明父类的构造函数和构造函数执行的顺序。
1、父类无缺省构造函数时,要显式调用父类的构造函数:
#include <iostream>
using namespace std;
class A {
public:
A(int y) {
std::cout << "A()" << std::endl;
}
~A() {
std::cout << "~A()" << std::endl;
}
};
class Point: public A{
public:
Point(int j, int k):A(3){ //显式调用父类的构造函数
this->x = j;
this->y = k;
std::cout << "Point(int j, int k)" << std::endl;
}
int x;
int y;
};
int main(int argc, char* argv[]) {
Point pt(1, 2);
return 0;
}
2、C++的初始化方式:直接初始化、拷贝初始化
直接初始化: =>调用构造函数的初始化
int i(5); //类中不可用
int i{5};
int i = int(5);
拷贝初始化: =>调用拷贝函数的初始化
int i = 1; //需要注意的是,这里发生了隐式类型转换(直接初始化是显式的、其实在C++中写成隐式的类型转换是不安全的,不利于代码阅读,建议养成使用显式的习惯)
需要注意的是:类是C++的相对于C的新增部分,其在类内成员变量初始化问题上,有一个规定,即类内部的()全被解释为函数调用,所以类内只能用=和{}进行初始化,而不能使用()。
3、基类的实例化顺序:
1、实例化成员变量。
2、执行类的构造函数。
4、子类的实例化顺序:先按继承顺序实例化每个父类,最后再实例化子类。(析构相反)
基类实例化 => 子类实例化 => 子类析构 => 基类析构
示例程序:
#include <iostream>
using namespace std;
class A {
public:
A(int a) { cout << "A():id = "<< this << endl; }
~A() { cout << "~A():id = " << this << endl; }
};
class B {
public:
B(int b) { cout << "B():id = " << this << endl; }
~B() { cout << "~B():id = " << this << endl; }
A obj{ 2 };
};
class Point :public B, A {
public:
Point(int x, int y):A(2),B(3) { cout << "Point():id = " << this << endl; }
~Point() { cout << "~Point():id = " << this << endl; }
};
int main(int argc, char* argv[]) {
Point pt(5, 5);
return 0;
}
打印如下: 先实例化B => 再实例化A => 最后实例化Point
A():id = 005AFBE4 // B
B():id = 005AFBE4 // B
A():id = 005AFBE6 // A
Point():id = 005AFBE4 //Point
~Point():id = 005AFBE4
~A():id = 005AFBE6
~B():id = 005AFBE4
~A():id = 005AFBE4
07、OOP的三大特征之类的多态
0、C++允许基类类型的指针指向子类的对象。
1、C++中,主要有两种类型的多态性:
编译时多态性(静态多态性):
函数重载、运算符重载、模板。
运行时多态性(动态多态性):
虚函数、多态继承。
2、主要是动态多态性:同一操作总用于不同的对象,可以有不同的执行结果
实现方式:基类的虚函数、子类函数重写(覆盖)、基类指针
关键字:基类中定义virtual函数、子类中重写并添加override,(override可以不加,但建议加上,因为加上override不仅可以进行规则检查,还可以增加代码的可读性)
#include <iostream>
class Shape {
public:
virtual void draw() {
std::cout << "Drawing shape...\n";
}
};
class Rectangle : public Shape {
public:
void draw() override {
std::cout << "Drawing rectangle...\n";
}
};
class Circle : public Shape {
public:
void draw() override {
std::cout << "Drawing circle...\n";
}
};
int main() {
Shape* shapes[2];
shapes[0] = new Rectangle();
shapes[1] = new Circle();
for (int i = 0; i < 2; i++) {
shapes[i]->draw();
}
return 0;
}
3、抽象类:除了使用虚函数,C++ 中还可以使用抽象类实现多态。抽象类是一种不能被实例化的类,它通常包含至少一个纯虚函数,纯虚函数在基类中声明时没有函数体,在派生类中必须被重写。由于抽象类不能被实例化,因此派生类必须实现所有纯虚函数才能被实例化
#include <iostream>
class Shape {
public:
virtual void draw() = 0;
};
class Rectangle : public Shape {
public:
void draw() override {
std::cout << "Drawing rectangle...\n";
}
};
class Circle : public Shape {
public:
void draw() override {
std::cout << "Drawing circle...\n";
}
};
int main() {
Shape* shapes[2];
shapes[0] = new Rectangle();
shapes[1] = new Circle();
for (int i = 0; i < 2; i++) {
shapes[i]->draw();
}
return 0;
}
08、C++ 初始化与赋值的区别
初始化的含义是在创建对象时赋予一个初值。
赋值的含义是将对象的当前值擦除掉,以一个新值代替。
区分方式:在对象创建时赋予值,叫初始化,否则即为赋值。
C++语言的初始化方式(以类类型为例):
<1>:直接初始化:调用的是与实参匹配的构造函数
<2>:拷贝初始化:调用的是拷贝构造函数
09、C++ 泛型编程的基础-模板
0、面向对象编程和泛型编程都是一种编程范式。
1、C++ Primer(第五版):面向对象编程(OPP)和泛型编程都能处理在编写程序时不知道类型的情况。不同之处在于:
OPP能处理类型在程序运行之前都未知的情况。(运行时知类型)
泛型编程中,编译时就能获得其类型了。(编译时知类型)
2、模板和宏定义很像,但是二者不同,例如:宏定义有直接替换而不检查的缺陷,而模板没有此缺陷。具体可以参考匿名类或结构体的使用。(类似但不同,模板更安全=>非替换=>根据参数反推实例化类型)
3、编译过程无法区分类是不是被实例化过:所以会被编译进可执行文件中。
编译过程可以区分类模板是不是被实例化过:所以会选择性的编译进可执行文件中。(类模板的转换发生在编译期间,如何编译期间未被实例化过,就不会被编译进可执行文件中)
4、 类定义==实例化==>对象 (类的实例化发生在运行期间)
类模板定义==实例化==>类定义==实例化==>对象 (类模板的实例化发生在编译期间)
5、代码示例
#include <iostream>
//1、auto:
// 此关键字也属于泛型编程的范畴
//2、模板:
// 输入可以是类型,也可以是变量
// 当模板的参数只有类型时,可以不指定类型,编译器根据输入推出类型T、生成对应的函数。
// 当模板的参数出现变量时,无论有没有类型,所有的模板的输入都必须手动指定。
//其它:定义模板函数,建议使用typename声明类型,定义模板类时,建议使用class声明类型。
template<typename T, int N>
int fun(const T param) {
return param + (T)N;
}
int main() {
cout << fun<int, 2>(3) << endl;
return 0;
}
二、C++算法
前言:子集问题很重要,对数组来说,要熟练学会求数组的组合。
01、排序算法
02、二分查找
题目:给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。
如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。
示例 1:
输入: nums = [1,3,5,6], target = 5
输出: 2
示例 2:
输入: nums = [1,3,5,6], target = 2
输出: 1
示例 3:
输入: nums = [1,3], target = 2
输出: 1
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int n = nums.size();
int left = 0,right = n-1,mid;
while(left<=right){
mid = left+((right-left)>>1);
if(target == nums[mid]) return mid;
else if(target>nums[mid]) left=mid+1;
else right=mid-1;
}
return left;
}
};
03、枚举连续子集
#include <iostream>
#include <vector>
//按同一起点枚举开始枚举,长度依次递增
vector<vector<int>> findSubNum1(vector<int> &nums, int n){
vector<vector<int>> twod_nums;
for(int left=0; left<n; left++){
for(int right=left; right<n; right++){
vector<int> vec(nums.begin()+left, nums.begin()+right+1);
//do sthing
twod_nums.push_back(vec);
}
}
return twod_nums;
}
//按同一阶数开始枚举,长度依次递增
vector<vector<int>> findSubNum2(vector<int> &nums, int n){
vector<vector<int>> twod_nums;
for(int len=1; len<=n; len++){
for(int left=0; left<=n-len; left++){
int right = left + len -1;
vector<int> vec(nums.begin()+left, nums.begin()+right+1);
//do sthing
twod_nums.push_back(vec);
}
}
return twod_nums;
}
int main() {
vector<int> nums = {2,1,5};
vector<vector<int>> twod_nums = findSubNum1(nums, nums.size());
//vector<vector<int>> twod_nums = findSubNum2(nums, nums.size());
for (auto v : twod_nums) {
for (auto num : v) {
cout << num << ',';
}
cout << endl;
}
}
=>findSubNum1输出 =>findSubNum2输出
2, 2,
2,1, 1,
2,1,5, 5,
1, 2,1,
1,5, 1,5,
5, 2,1,5,
04、寻找最长连续字符子集
举例:string str = "GRAAHHCCDEKFF";(无小写字母),的最长连续字符子集为"CDEFGH".
初始:排序 => str = "AACCDEFFGHHKR", ?求最长连续字符子集
//查找一个字符串的连续字串中的连续字符最长的子串
vector<string::iterator> findMaxLength(string& s) {
auto pre_left = s.begin(), pre_right = pre_left;
auto cur_left = s.begin(), cur_right = cur_left;
int max_dif = 0;
while (cur_right != s.end()) {
cur_right++;
if ((cur_right == s.end()) || (*cur_right!=*(cur_right-1) + 1)) {
int dif = cur_right - cur_left;
if (dif > max_dif) {
max_dif = dif;
pre_left = cur_left;
pre_right = cur_right - 1;
}
cur_left = cur_right;
}
}
return{ pre_left, pre_right };
}
05、二叉树问题
0、二叉树的种类:
1、满二叉树:最后一层全是叶子节点,其他层全是二叉节点
2、完全二叉树:除了最后一层,每一层都是满的,且最后一层的节点从左到右依次填充。
3、平衡二叉树:左子树和右子树的高度差不能大于1
2、二叉树的高度与深度:
06、贪心算法(局部最优推全局最优)
07、回溯算法(解决排列组合相关问题)
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2
输出:
[ [2,4],[3,4],[2,3],[1,2],[1,3],[1,4] ]
示例 2:
输入:n = 1, k = 1
输出:[[1]]
class Solution {
public:
vector<vector<int>> vec_sum;
vector<int> vec_sub;
void backtracking(int n, int k, int begin){
if(vec_sub.size()==k){
vec_sum.push_back(vec_sub);
return;
}
for(int i=begin; i<=n-(k-vec_sub.size())+1; i++){
vec_sub.push_back(i); //装vec[begin]
//装vec[begin+1],(装vec[begin+2]时=k满足,故没装成;=>pop pop;
//保存结果并返回,此轮结束。
backtracking(n,k,i+1);
vec_sub.pop_back(); //撤销选择
//每次开始时vec_sub的长度都为0
}
}
vector<vector<int>> combine(int n, int k) {
if(k<=0||n<k){ return {{}}; }
backtracking(n,k,1);
return vec_sum;
}
};