目录
一.C++基础
1.第一个C++程序:
可能有些同学跟我一样,一直以来学习的都是基础的C语言,而在接触蓝桥杯这类相比较正规的竞赛时会发现它所使用的程序规范与基础的C语言大相径庭,但没关系的,虽然他使用的规范隶属于C++范畴,但只要掌握以下几条差异,C++于我们也不会再陌生,光说不算,先直接来看一下我们的第一个C++程序:
cpp #include <iostream> //头⽂件 using namespace std; //使⽤std的名字空间 int main() //main函数 { cout << "hello world!" << endl; //输出:在屏幕打印"hello world!" return 0; }
2.头文件:
ok,接下来我就来详细说说这其中的一些区别,当然最先注意到的就是包含的头文件的差异了:
前⾯的代码中,写的 #include ,就是在包含头⽂件,头⽂件的名字叫:iostream ,
使用 #include <> 的形式进行包含
iostream 文件中的 io 指的是输⼊(进入程序的信息,简单理解就是可以给程序输⼊数据)和输出(从程序中输出的信息,简单理解就是程序 会打印数据在屏幕上)。
在 C++ 程序中要完成输⼊和输出的操作会涉及到 iostream 文件中的多个定义,所以就要包含这个 头⽂件的
比如:代码中的 cout 是输出流对象,就是⽤来完成数据输出的,就需要包含头文件。
简单些理解:iostream这个头文件就当于是一个我们之前学习C语言里无数头文件的一个集合,为的就是使我们对头文件的包含的使用更加方便和简洁
注:
- 在C语⾔中头文件的扩展名是 .h ,但是C++中的⽤法发生了⼀些变化,对⽼式C的头⽂件保留了扩展名 .h ,但是C++自己的⽂件没有扩展名了,如原来的C语⾔头⽂件:
- 有些C的头⽂件被转换成C++头⽂件,这些⽂件名被重命名,去掉了.h扩展名,并在⽂件名的前⾯加 上了前缀c(表示来⾃C语⾔);例如:C语⾔中有关数学的头⽂件名字是 math.h ,在C++中就 是cmath ,当然还得注意,有时头⽂件的C语⾔版本和C++版本相同,⽽有时候,新版本做了⼀些 修改:
3.cin和cout初识:
cout << "hello world!" << endl; 这句代码在上⾯的程序中是最重要的代码,其他所有的 代码都是为了编写这句代码。
cout 是标准输出流对象 (针对控制台,也就是屏幕),其实还有标准输⼊流对象cin(针对的是键 盘)
即cout 告诉程序把后⾯双引号中的内容打印到标准输出设备(屏幕) 上,双引号中的内容可以替换
cpp#include<iostream> using namespace std; int main() { int num; cin >> num; //获取标准输⼊ cout << num << endl; //对获取到的结果标准输出 return 0; }
总结:1.cin 和 cout 是全局的流对象,cin 负责输⼊数据(scanf) , cout 负责输出数据(printf)
2.endl 是C++中⼀个特殊的操作符,效果是换行和刷新缓冲区,使⽤时必须包含在 iostream 头文件里
3.<<是流插⼊运算符 ,和 cout 配合使用,>> 是流提取运算符 ,和 cin配合使⽤,两者容易混 淆,⼤家⼀定要仔细区分,不可混用
使⽤C++输⼊输出更⽅便,不需要像 printf / scanf 输⼊输出时那样,需要⼿动控制格式。 C++的输⼊输出可以⾃动识别变量类型。(后面会提到到,慢慢体会就好):
cpp#include <iostream> using namespace std; int main() { float score = 0; cin >> score;//直接读取的就是浮点数 cout << score;//直接输出的就是浮点数 return 0; }
4.命名空间:
using namespace std; 这句代码的意思是:使⽤名字空间 std (名字空间也叫命名空间), 为了理解什么是名字空间,名字空间要解决什么问题,先看⼀下下面的例子:
在C++中,变量、函数和类都是**⼤量存在** 的,这些变量、函数和类的名称如果都存在于全局作⽤域中, 可能会导致很多冲突 。使⽤ 名字空间 的**⽬的是对标识符的名称进⾏隔离,以避免命名冲突或名字污染** , namespace 关键字的出现就是针对这种问题的。 std 是C++标准库的名字空间名,C++将标准库的定义实现都放到这个命名空间中 ,当我们需要使用标准库中的内容时,就需要加上: using namespace std ;当有了这句代码的时候,表示命名空间 std 中信息都是可见和可⽤的,比如:cin 、 cout 、 endl 等:
当然使用using namespace std;是⼀种简单粗暴的做法,直接这样使⽤,就意味着后续在std这个名字空间中的各种定义都可以直接使⽤,但是我们往往只是使⽤部分 。所以名字空间其实也可以这样使⽤:
cpp#incldue <iostream> int main() { std::cout << "hello world" << std::endl; return 0; }
代码中的std::cout的意思就是使⽤std名字空间中的cout
欧克,到这里为止再看一下最初的C++程序,是不是就好理解多了:
cpp#include <iostream> //头⽂件 using namespace std; //使⽤std的名字空间 int main() //main函数 { cout << "hello world!" << endl; //输出:在屏幕打印"hello world!" return 0; }
二.顺序表和vector(STL)
说完了上面C++的基础格式,接下来就是正式的蓝桥杯知识点的干货分享了
1.顺序表的基本操作:
其实顺序表的基本操作无非就是我们老生常谈的那几种尾插尾删,头插头删,指定位置插入和删除和对某一个元素的查找,这些在C语言基础里我都有提及,详细可以看我附在下面的这篇文章:
https://blog.csdn.net/2403_87691282/article/details/144203616?spm=1001.2014.3001.5501
方便起见,我还是把简化的一些操作附在下面,以便阅览:
cpp// 打印顺序表 void print() { for(int i = 1; i <= n; i++) { cout << a[i] << " "; } cout << endl << endl; } // 尾插 void push_back(int a[], int& n, int x) { a[++n] = x; } // 头插 void push_front(int x) { // 1. 先把 [1, n] 的元素统⼀向后移动⼀位 for(int i = n; i >= 1; i--) { a[i + 1] = a[i]; } // 2. 把 x 放在表头 a[1] = x; n++; // 元素个数 +1 } // 在任意位置插⼊ void insert(int p, int x) { //把 [p, n] 的元素统⼀向后移动⼀位 for(int i = n; i >= p; i--) { a[i + 1] = a[i]; } a[p] = x; n++; } // 尾删 void pop_back() { n--; } // 头删 void pop_front() { // 1. 先把 [2, n] 区间内的所有元素,统⼀左移⼀位 for(int i = 2; i <= n; i++) { a[i - 1] = a[i]; } n--; } // 任意位置删除 void erase(int p) { // 把 [p + 1, n] 的元素,统⼀左移⼀位 for(int i = p + 1; i <= n; i++) { a[i - 1] = a[i]; } n--; } // 按值查找 int find(int x) { for(int i = 1; i <= n; i++) { if(a[i] == x) return i; } return 0; } // 按位查找 int at(int p) { return a[p]; } // 按位修改 int change(int p, int x) { a[p] = x; } // 清空操作 void clear() { n = 0; }
2.封装静态顺序表:
所谓封装静态顺序表就是当我们在面对需要创建好几个顺序表并且对它们进行操作及以下情况 时而产生的简化操作的行为:
可见,当涉及到多个顺序表时,虽然上述的代码可以套用,但还是略显麻烦,这个时候就不由得发问了:博主博主,这些函数固然好用,但有没有什么其他更简便的套用方式,有的兄弟,有的,这么方便的用法当然是有的:
cpp#include <iostream> using namespace std; const int N = 1e5 + 10; // 将顺序表的创建以及增删查改封装在⼀个类中 class SqList { int a[N]; int n; public: // 构造函数,初始化 SqList() { n = 0; } // 尾插 void push_back(int x) { a[++n] = x; } // 尾删 void pop_back() { n--; } // 打印 void print() { for (int i = 1; i <= n; i++) { cout << a[i] << " "; } cout << endl; } }; int main() { SqList s1, s2; // 创建了两个顺序表 for (int i = 1; i <= 5; i++) { // 直接调⽤ s1 和 s2 ⾥⾯的 push_back s1.push_back(i); s2.push_back(i * 2); } s1.print(); s2.print(); for (int i = 1; i <= 2; i++) { s1.pop_back(); s2.pop_back(); } s1.print(); s2.print(); return 0; }
在上述代码里,博主使用了class(类的运用) 对几个函数进行封装,这样在以后对顺序表的操作时,就可以直接使用**"."**进行各种简洁的运用了
当然这里还需再提一下class和public的使用了(蓝桥杯作为一个应试考试,好些代码的具体原理就不再在这里进行深究了,未来我会具体介绍,但也仅供了解):
在C++中,class是定义类的关键字,而public是访问修饰符之一
以下是详细解释:
class(类)
类是C++中的基本构造块,用于定义对象的属性和行为
类使用class关键字声明,后面跟着类名和类体,类体中包含成员变量和成员函数2。
public(公有)public是类的访问修饰符之一,表示该成员(变量或函数)是公有的,可以被类的外部访问
公有成员在类的内部和外部都可以被访问,相当于C语言中的struct结构体成员ps**:**
在C++中,类和结构体(struct)都是用于定义复合数据类型的构造,它们都可以包含成员变量和成员函数。然而,尽管它们在许多方面相似,但也有一个关键的区别:
即默认访问权限
类(class):在类中,默认的访问权限是私有的(private),这意味着除非明确指定为public或protected,否则类的成员是不可从类外部访问的。
结构体(struct):在结构体中,默认的访问权限是公有的(public),这意味着结构体的所有成员默认都是可以从外部访问的,除非它们被明确指定为private或protected
当然我这边讲封装肯定不仅仅是为了简洁,更多的还是为我接下来介绍SYL做一个铺垫
当然STL具体是啥我还是先说明一下:
提供高效的数据结构:
STL包含了多种高效的数据结构,如vector(动态数组)、list(双向链表)、map(映射/字典)、set(集合)等。这些数据结构都是经过精心设计和优化的,可以在不同的场景下提供高效的数据存储和访问。
实现常用的算法:
STL提供了一系列常用的算法,如排序、搜索、合并、拷贝等。这些算法都是以模板函数的形式提供的,可以适用于不同的数据类型和容器。通过使用STL的算法,可以避免重复编写常见的算法代码,提高开发效率。
3.动态顺序表--vector:
在之前C语言的学习过程中,一提到动态顺序表就不由得会回忆起被malloc和free,new和deletae支配的恐惧,而这里需要强调一点的就是竞赛代码不同于我们之前学的工程代码 ,什么是工程代码,就是以malloc为首的一系列相比来说在竞赛中使用效率不高而且容易超时的一系列函数,因此当我们在竞赛过程中使用动态顺序表的时候就有了一种更好的方式:
C++ 的STL 提供了⼀个已经封装好的容器vector , 有的地⽅也叫作变⻓数组 , vector 的底层就是⼀个会⾃动扩容的顺序表 ,其中创建以及增删查 改等等的逻辑已经实现好了 ,并且也完成了封装, 接下来就重点了解以下vector 的使用:
(1)创建vector:
cpp
#include <vector> // 头⽂件
using namespace std;
const int N = 20;
struct node
{
int a, b, c;
};
// 创建
void init()
{
vector<int> a1; // 创建⼀个空的可变⻓数组
vector<int> a2(N); // 指定好了⼀个空间,⼤⼩为 N
vector<int> a3(N, 10); // 创建⼀个⼤⼩为 N 的 vector,并且⾥⾯的所有元素都是 10
vector<int> a4 = { 1, 2, 3, 4, 5 }; // 使⽤列表初始化,创建⼀个 vector
// <> ⾥⾯可以放任意的类型,这就是模板的作⽤,也是模板强⼤的地⽅
// 这样,vector ⾥⾯就可以放我们接触过的任意数据类型,甚⾄是 STL
vector<string> a5; // 放字符串
vector<node> a6; // 放⼀个结构体
vector<vector<int>> a7(N);
vector<int> a8[N];
}
大家仔细看的话会发现我最后俩没写注释,为啥,因为我觉得这俩在理解上有些不太容易,所以我就单独把它们拎出来做解释:
就正如上图(字可能有些丑,凑合看看吧(捂脸))所示,可以理解为前者的主体时vector<int>类型的变长数组,而后者则是已顺序表为主体来存放变长数组
其实在C++中,vector<vector<int>> a7(N); 和 vector<int> a8[N]; 是两种不同的方式来创建数组或数组的数组(即二维数组 ),但它们依旧有着本质的区别:
vector<vector<int>> a7(N);
这是一个使用vector容器创建的二维动态数组。vector是C++标准模板库(STL)中的一个序列容器,它可以动态地管理内存,根据需要自动调整大小。这里的a7是一个包含N个vector<int>的vector,即一个二维数组,其中每一行都是一个可以独立调整大小的vector<int>
优点:
动态调整大小:每一行都可以根据需要独立增加或减少元素。
内存管理自动化:vector会自动管理内存,减少内存泄漏的风险。
缺点:可能的性能开销:由于动态调整大小和自动内存管理,可能会引入一些性能开销。
不是连续内存:vector的底层实现通常是一个指向动态分配内存的指针数组,因此二维vector的元素在内存中不是连续存储的。
vector<int> a8[N];
这是一个静态数组,其中每个元素都是一个vector<int>,不同于第一个例子,这里的数组大小N必须在编译时就已知,且整个数组的大小在创建后是固定的。
优点:
可能的性能优势:由于是静态数组,其大小在编译时确定,可能在某些情况下提供更好的性能,尤其是在访问连续内存时。
缺点:固定大小:数组的大小在创建后不能改变,这限制了其灵活性。
非标准C++:使用静态数组的方式在某些情况下可能不符合现代C++的最佳实践,尤其是在需要动态调整大小或跨函数共享数据时。
总结来说,选择哪种方式取决于具体需求,如果需要一个大小可变的二维数组,或者希望自动管理内存,那么vector<vector<int>>是一个好选择,如果知道数组的大小并且希望利用静态数组可能的性能优势,那么vector<int> a8[N];也可以考虑,不过,在现代C++编程中,推荐使用vector和智能指针等STL组件来管理动态数据结构,以减少内存管理错误和提高代码的可维护性
(2)size和empty:
size :返回实际元素的个数
empty :返回顺序表是否为空,因此是⼀个bool 类型的返回值
a. 如果为空:返回true
b. 否则,返回false
时间复杂度:O(1)
以下是具体使用方式,接下来我介绍的几个都是vector里常用的关键字,了解会用就行
cpp
void test_size()
{
// 创建⼀个⼀维数组
vector<int> a1(6, 8);
for(int i = 0; i < a1.size(); i++)
{
cout << a1[i] << " ";
}
cout << endl << endl;
// 创建⼀个⼆维数组
vector<vector<int>> a2(3, vector<int>(4, 5));
for(int i = 0; i < a2.size(); i++)
{
// 这⾥的 a2[i] 相当于⼀个 vector<int> a(4, 5)
for(int j = 0; j < a2[i].size(); j++)
{
cout << a2[i][j] << " ";
}
cout << endl;
}
cout << endl << endl;
}
(3)begin和end:
begin :返回起始位置的迭代器(左闭)
end :返回终点位置的下⼀个位置的迭代器(右开)
利⽤迭代器可以访问整个vector ,存在迭代器的容器就可以使⽤范围for 遍历
说到迭代器,还有一点不得不提一下我们的auto了:
在C++11及更高版本中,auto 关键字被引入以支持类型自动推导。这意味着编译器可以根据初始化表达式自动推断变量的类型,而无需指定,auto 的使用可以简化代码,尤其是在处理复杂类型或模板时:
cppint x = 10; auto y = x; // y 的类型被推导为 int
当使用STL容器(如 vector, map, set 等)的迭代器时,auto 可以避免冗长的迭代器类型声明:
cppstd::vector<int> vec = {1, 2, 3, 4}; for (auto it = vec.begin(); it != vec.end(); ++it) { // it 的类型被推导为 std::vector<int>::iterator std::cout << *it << std::endl; }
还可以与语法糖结合在一起使用:
cppfor (auto x : a) { cout << x << ""; } cout << endl;
当然这里的a是指一个vector<int>类型的变长数组
上面这两行代码都可以实现对a数组的遍历与打印
(4)front和back:
front :返回**⾸元素**
back :返回尾元素
时间复杂度:O(1)
cpp// ⾸元素和尾元素 void test_fb() { vector<int> a(5); for(int i = 0; i < 5; i++) { a[i] = i + 1; } cout << a.front() << " " << a.back() << endl; }
这个使用并不难,看一下了解即可
(5)resize和clear:
**1.resize:**修改vector 的大小
• 如果⼤于原始的大小,多出来的位置会补上默认值,⼀般是0
• 如果⼩于原始的大小,相当于把后⾯的元素全部删掉。 时间复杂度:O(N)
**2.clear:**清空vector
底层实现的时候,会遍历整个元素,⼀个⼀个删除,因此时间复杂度:O(N)
cpp// resize void test_resize() { vector<int> a(5, 1); a.resize(10); // 扩⼤ print(a); a.resize(3); // 缩⼩ print(a); } // clear void test_clear() { vector<int> a(5, 1); print(a); a.clear(); cout << a.size() << endl; print(a); }
尾言:
欧克,全文终