CPP 速成
前言
下方所有内容均基于 c++ primer 得到,内容为 C11 版本 以代码块+注释的方式学习,不讲多余废话理论
表达式
一元二元三元运算符
#include<bits/stdc++.h>
using namespace std;
void cp10_1(){
bool b = true; // bool值的true表示1
bool b2 = -b; // -b即-1,但是bool不接受负数,故转换为1,所以导致b2仍然为true
int c1 = 21/6; // 结果为3,因为俩整数相除且结果为整数,直接掐掉小数部分且不四舍五入
int c2 = 10%1; // 取余当且仅当左右两边都是整数类型才可以使用
cout << -10/2 << endl; // 除法的结果符号依据正常运算标准,所以这里-10除以2结果为-5
cout << 10%(-3) << endl; // 取余结果符号依据第一个数字的符号,所以10对-3取余结果为1
}
void cp10_2(){
int a=10;
if(a) cout<<"hey"<<endl; // 直接写变量作为判据,当a==0时为假,不等于0是就是真
int v1,v2;
v1=v2=0; // 赋值运算符满足右运算律,所以不用怕没有初始化导致错误,这里v1=v2=0
}
void cp10_3(){
int a=100;
cout << a++ << " " << ++a << endl;
/* a++运行机理:
先把原来的a值复制一个副本并保存下来
之后对a进行自增操作
然后返回保存的副本(也就是没有+1的原始值)
++a运行机理:
直接a自增后返回自增后的a值
*/
}
void cp10_4(){
// 循环输出向量内容的另一种方法
vector<int> v = {1,2,3,4,5,6,7,8};
auto num = v.begin();
while(num!=v.end() && *num>=0){
cout << *num++ << endl;
}
/* 对*num++的解释:
因为自增运算符的优先级比解引用符号高,所以可以看出*num++和*(num++)是等价的
之后根据之前讲的,a先把原值存成副本再自增后返回副本
所以解引用符合解的是num指向的内容而不是num++指向的内容
(此方法在简化代码方面及其常用)
*/
}
void cp10_5(){
// (*p).size()和p->size()含义一致
// 如果写成*p.size()就会先对指针取出值,但因为指针不属于值,所以会报错
string s1 = "not in here", *p=&s1;
auto u = s1.size();
u = (*p).size();
u = p->size();
}
void cp10_6(){
// 三元运算符也可以嵌套,下面计算了分数值并根据分数的大小从而输出不同的值
int grade=70;
string result = (grade>90) ? "excellent" : (grade>60) ? "pass" : "fail";
cout << result << endl;
}
// 使用static_cast代替(int)进行强转类型可以避免编译器警告,但依然会损失精度
int k = 0;
double c = static_cast<double>(k);if 与 for 规范
#include<bits/stdc++.h>
using namespace std;
void cp11_1(){
// 空语句独占一行,表示什么都不做
for(int i=0;i<10;i++){
;
}
}
void cp11_2(){
// 当if语句下都仅只有一行且不用花括号时就会导致可能存在的“悬垂else”
// 悬垂else就如下面的else一样可能匹配第一个if或第二个if,但C编译器会按照就近原则,即选第二个if
// 把所有if语句下加上花括号即可
int a=10,b=20;
if(a<b)
if(a+5>b);
else
cout<<"error"<<endl;
}
void cp11_3(){
// switch可以一连串匹配多个case,除此之外他们还可以写在一行内
char c='a';
int num=0;
switch(c){
case 'a': case 'b': case 'c':
num++;break;
default:
break;
}
// 在某个case里面定义一个语句块,就会把里面的变量作用域限制在该case中,其他case无法使用这些变量
// 如果不加语句块,在语句判断时会导致跳转到另一case使用该变量但变量未初始化从而导致错误
bool b=true;
switch(b){
case true:
{
int k=0;
k++;
}
break;
case false:
break;
}
}
void cp11_4(){
// goto语句强烈建议不要使用,虽然他也可以作为类似循环来使用,但还是要注意变量初始化问题
int c=0;
loop:
if(++c<10)goto loop;
cout<<c<<endl;
}错误抛出与异常
#include<bits/stdc++.h>
using namespace std;
void cp12_1(){
// 在try代码块里写入判断条件,并使用throw函数抛出指定异常
// 每个异常都定义于某头文件内,并且throw该异常是需要传递字符串参数,表示抛出异常的文本
// catch后面的参数就是接收抛出异常的名字,且必须为该异常指定别名,为接下来的处理做准备
// err.what()中的what存在于每个异常类型中,他表示需要传入一个字符串用作输出
int a1=100,a2=200;
try{
if(a1!=a2)
throw runtime_error("a1 is equals a2!!!\n");
}catch(runtime_error err){
cout<<err.what()<<"please change the value of them"<<endl;
}
}
void cp12_2(){
// 常用的标准异常类定义于stdexcept头文件里面
// new头文件定义了bad_alloc异常类型
// type_info头文件定义了bad_cast异常类型
// 特别注意exception也是一个异常类,但他不允许和其他异常类一样用字符串初始化,仅能默认初始化
}动态缓存
智能指针 shared_ptr
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<set>
#include<string>
#include<stack>
#include<queue>
#include<memory>
using namespace std;
// shared_ptr智能指针,它类似vector,尖括号内为模板,它存在于头文件memory里面
// 创建一个智能指针shared_ptr,他指向一个string对象
// s1检测指针是否为空,而s1->empty()检测指针所指对象是否为空,使用解引用来赋值,因为是指针!
// 下面if内表示:若指针非空且包含一元素,那么将该元素赋值为helloworld
void sp1() {
shared_ptr<string> s1;
if (s1 && s1->empty()) *s1 = "helloworld";
}
// make_shared根据指定类型初始化并返回一个shared_ptr,它常用来初始化shared_ptr
// 注意到make_shared后面尖括号和shared_ptr是一致的!
// 常用auto代替复杂的类型指定
void sp2() {
shared_ptr<int> si = make_shared<int>(123);
auto p6 = make_shared<vector<string>>();
}
// shared_ptr对象可以被多次引用,每次被引用该对象自身计数器+1
// 当shared_ptr对象是局部变量且函数结束后内存会在计数器==1时自动释放(因为>1表示还有东西引用它)
// 只要计数器不等于0,智能指针就不会自动释放内存并销毁
void sp3() {
}new 内存分配
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<set>
#include<string>
#include<stack>
#include<queue>
#include<new>
#include<memory>
using namespace std;
// 直接管理内存
// new出的是匿名对象,你可以不传参从而使用默认初始化,比如第一行
// 必须要用指针,来指向匿名对象所分配好的内存
// 对于vector这种类型的,也可以直接使用列表初始化的方法
void n1(){
int *i1 = new int;
int *i2 = new int(10);
vector<int> *vec = new vector<int>{1,2,3,4,5};
}
// 用new分配const对象是合法的,但必须要进行初始化!
void n2(){
const int *cn = new const int(123);
}
// bad_alloc和nothrow都定义在头文件new里面
// 电脑内存不足时就会抛出bad_alloc
// 使用定位new,如下方的(nothrow)就表示不抛出bad_alloc错误,而是指定其为空指针,不分配内存
// 如果内存充足,就不触发nothrow,依然开辟正常内存存储
void n3(){
int *nn = new (nothrow) int;
}
// reset让一个指针重新指向新的对象,仅当指针为shared_ptr类型时才可以使用
// 使用reset后会更新引用计数,必要情况下还会释放指针所指对象
void n4(){
shared_ptr<int> p(new int(20));
p.reset(new int(100));
}
// delete释放指针
// 当new后,delete前发生异常,内存无法释放,因为除了函数n5外没有任何东西指向这个内存
void n5(){
int *p = new int(100);
delete p;
}unique_ptr
#include<memory>
using namespace std;
// unique_ptr解析
// unique_ptr依然在头文件memory里面
// unique_ptr只允许指向一个对象
// 它不支持直接赋值,也不允许直接用圆括号进行拷贝,需要特殊的方法
// 第二行为固定格式,将p1所有权转让给p2,p1.release()表示释放指针,本代码等同拷贝效果
// reset释放引用,第四行将引用指向了p2
void up1(){
unique_ptr<int> p1(new int(10));
unique_ptr<int> p2(p1.release());
unique_ptr<int> p3;
p3.reset(p2.release());
}class fun1_1 {
public:
fun1_1() = default;
fun1_1(const string &str): name(str) {}
string getName() const {
return this->name;
}
void setName();
private:
string name;
};
int main() {
// unique_ptr实例化对象
unique_ptr<fun1_1> f1 = make_unique<fun1_1>("asd");
// unique_ptr实例化对象后得到其对象指针,需要按照操作指针的方式获取其值
string s = f1->getName();
cout << s << endl; // asd
// get获取对象原始指针
fun1_1* f2 = f1.get();
// 直接解引用获取对象引用
fun1_1& f3 = *f1;
// 同理,基于指针的操作
f2->getName();
// 同理,基于引用的操作
f3.getName();
return 0;
}动态数组
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<set>
#include<string>
#include<stack>
#include<queue>
#include<new>
#include<memory>
using namespace std;
// 动态数组
// 使用方括号定义一个数组,但没有初始化,并且此方法都会返回一个指针
// 方括号后加一对圆括号进行默认初始化,第二行为数组中每一个元素初始化为0
// 方括号内必须是整数且非常量
// 第三行:可以new一个[0]数组,但是无法解引用
void da1(){
int *pia = new int[10];
int *npia = new int[10]();
char *ch = new char[0];
}
// delete [] name 释放数组指针(不要把方括号丢了)
// unique_ptr也可以初始化动态数组,且用release释放它(代替delete)
// unique_ptr指向动态数组时可以使用下标运算符来取出元素
void da2(){
int *it = new int[10];
delete [] it;
unique_ptr<int[]> ui(new int[10]);
ui.release();
for(int i=0;i!=10;i++) ui[i]=1;
}
// shared_ptr也可使用动态数组,但是需要lambda函数定义一个自己的删除器
void da3(){
shared_ptr<int> sp(new int[10], [](int *p) {delete[] p;});
sp.reset();
}allocator
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<set>
#include<string>
#include<stack>
#include<queue>
#include<memory>
using namespace std;
// allocator类 -> 定义于头文件memory中
// allocator也是一个模板,他接受一个类型参数
// 使用allocate函数分配给定数量对象的内存,下面分配5个string内存
void ac1(){
allocator<string> str;
auto const ss = str.allocate(10);
}函数
main 形参
#include<bits/stdc++.h>
using namespace std;
int main(int argc, char **argv){
for(int i=0;i<=argc;i++){
cout<<argv[i]<<endl;
}
return 0;
}
/* main内形参详解
假定我们来到源代码(代码已编译成exe)文件夹下并输入代码CP13 -c -d name
CP13必须有,表示我们要执行的exe文件的名字,后面几个是附加参数,有3个
argc表示传入的参数总数,可见这里有4个参数(包括文件名)
argv则是存参数的数组,argv[0]存文件名"CP13",以此类推argv[1]存"-c"......
但是argv会在最后一个元素自动加0表示结尾,故argv[5]=0,所以我们上面的for特地用了i<=argc
*/可变长参数
#include<bits/stdc++.h>
#include<initializer_list>
using namespace std;
// initializer_list<string> list表示接收一个可变长的参数,参数类型都是string
// initializer_list可以类似vector一样使用迭代器的方式输出里面的成员
// 下面的代码就用了此方法输出所有成员
// 注意main里面的调用代码,必须有一花括号把传入参数括起来,里面写多少个参数都无所谓,因为可变长
// 事实上可以再加入任意一个形参,此时调用该函数时就需要按照顺序来输入实参了
void cp14_1(initializer_list<string> list){
for(auto beg=list.begin();beg!=list.end();++beg){
cout<<*beg<<" ";
}
cout<<endl;
}
// void函数里面仍然可以有return,但是后面不能接除了void类型的任何返回值
// 我们可以在任意一个判断或者循环条件中使用return达到退出函数的效果
// 注意,对于有返回值的函数,仅在for里面写return是不够的,必须要在函数最末尾加上return!!!
void cp14_2(){
for(int i=0;i<10;i++){
if(i==4) return;
}
return;
}
void cp14_3(){
}
int main(){
cp14_1({"hello","my","dear","cpp"});
return 0;
}形参引用与指针
#include<bits/stdc++.h>
using namespace std;
void cp15_1(const int a){
// 当形参存在顶层const时,编译器会自动忽略掉形参的const
// 意思就是我们可以为形参传递一个const int或者int类型的实参进去而不报错
// 所以我们没法在写一个void cp15_1(int a)来重载,因为系统会自动去掉顶层const,导致俩函数形参一致
}
// 我们可以使用标记确定数组的长度
// 下面的判断表示遇到空字符后停止输出,这样就可判别字符串中有多少个字符
void cp15_2(const char* c){
if(c)
while(*c)
cout<<*c++<<endl;
}
// 传递数组的首指针和尾后元素指针来确定数组长度
void cp15_3(const int* a,const int*b){
while(a!=b){
cout<<*a++<<endl;
}
}
// 定义一返回值为引用的函数
// 这样就避免了对string对象的任何拷贝,这里全程仅传递并使用引用来处理所有事情
const string &cp15_4(const string &s1,const string &s2){
return s1.size()>=s2.size() ? s1 : s2;
}尾置返回类型
#include<bits/stdc++.h>
using namespace std;
void cp16_1(){
// 定义一个数组指针parr,他指向一个含有10个元素的数组
int arr[10];
int (*parr)[10] = &arr;
}
// cpp11尾置返回类型,用来声明复杂函数(这里使用返回值为数组指针的函数作为示范)
// 小箭头后面写返回值类型,前面写函数名字和形参,最前面的auto是为了适配小箭头后面的数组指针的
// 第一行和第二行(传统表达法)含义完全一致,但是第二行真的太难理解了
auto cp16_2(int i) -> int(*)[10];
int (*cp16_2(int i))[10];
// 若已经确定函数要返回那个数组指针,则可以使用decltype来确定返回值
// 因为decltype(odd)表示:含义5个元素的int类型数组,故后面还要加*表示这是数组指针
int odd[]={1,2,3,4,5};
int badly[]={6,7,8,9,0};
decltype(odd) *cp16_3(int i){
return (i%2) ? &odd : &badly;
}函数重载
#include<bits/stdc++.h>
using namespace std;
// 重载函数必须要形参数目或者类型不一致
// 但返回类型不同但形参完全一致是没办法重载的!
void cp17_1(double a){}
void cp17_1(int a){}
// 前情提要:一个声明顶层const和一个没声明顶层const是无法区分的,因为系统编译时自动去除const
// 所以我们不能再写void cp17_2(int i){}
void cp17_2(const int i){}
// 但给形参加了指针或引用后,再加const就变成底层的了,这时完全可以重载
// 我们可以给一被const修饰的形参传入非常量,他会自动转换类型
void cp17_3(const int& a){}
void cp17_3(int &a){}形参默认值与 constexpr
#include<bits/stdc++.h>
using namespace std;
// 使用:类型 变量名=值 来设置默认值
// 不能仅对前面赋予默认值而不对后面的值赋予默认值,所以去掉第一行只保留第二行就报错
// 因最后一个参已被第一行赋初始值,所以第二行赋初始值绝对不可以对他重复定义
void cp18_1(int,double,char c='a');
void cp18_1(int a=1,double b=1.43,char);
/* constexpr函数很特殊
1. 当他修饰一个函数时,函数体内有且仅有一个return语句
2. 被修饰的函数的形参、返回值、return内的所有内容都必须是字面量形式
3. constexpr函数隐式定义为内联函数
*/
constexpr size_t cnt(size_t st){
return st*100;
}
constexpr size_t s = cnt(16);
int main(){
// 直接使用函数的所有默认值
cp18_1();
// 如果我们需要修改最后一个形参的默认值,必须要把前面的所有参数都填上不能留空!
cp18_1(1,4.2,'k');
return 0;
}
void cp18_1(int a,double b,char c){}面向对象
基本类组成
#include<bits/stdc++.h>
using namespace std;
/* 讲解const和this的作用
1.name()是成员函数声明
2.this可以简单地理解为“这个类”,在一个类中不可以出现任何名字为this的变量或函数
3.this原意是指向类类型非常量类型的常量指针,他的类型:cp19_1 *const
4.name()后加const则表示一个 常量成员函数
5.常量成员函数表示不能改变调用它的对象的内容
*/
class cp19_1{
string name() const {return this->getName;}
string getName;
void setName();
};
// 编译器初始化顺序:先编译成员声明然后才编译成员函数体
// 所以成员函数体可以任意使用类内其他成员(成员函数体就是void name(){}花括号里面的玩意)实例化一个对象
#include <memory>
class MyClass {
public:
int value;
MyClass(int v) : value(v) {}
void display() { std::cout << "Value: " << value << std::endl; }
};
// 1. 在栈上实例化对象
MyClass stackObj(42);
// 2. 在堆上实例化对象
MyClass* heapObj = new MyClass(42);
// 3. 使用std::make_unique在堆上实例化对象
auto uniqueObj = std::make_unique<MyClass>(42);
// 4. 使用std::make_shared在堆上实例化对象
auto sharedObj = std::make_shared<MyClass>(42);
// 5. 创建对象数组
MyClass arrayObj[5];
// 6. 创建对象指针数组
MyClass* ptrArray[5];
for (int i = 0; i < 5; ++i) {
ptrArray[i] = new MyClass(i);
}
// 7. 使用std::vector管理对象
std::vector<MyClass> vecObj;
vecObj.push_back(MyClass(10));
vecObj.push_back(MyClass(20));
// 8. 使用std::array管理对象
std::array<MyClass, 5> arrObj{{MyClass(1), MyClass(2), MyClass(3), MyClass(4), MyClass(5)}};
// 9. 使用列表初始化
MyClass listInitObj{100};
// 10. 使用构造函数进行复杂对象的实例化
MyClass constructedObj = MyClass(24);
// 11. 通过赋值创建std::unique_ptr的副本(实际上是移动操作)
std::unique_ptr<MyClass> anotherUniqueObj = std::move(uniqueObj);
// 12. 通过赋值创建std::shared_ptr的副本
std::shared_ptr<MyClass> anotherSharedObj = sharedObj;
// 清理分配在堆上的资源
delete heapObj;
for (int i = 0; i < 5; ++i) {
delete ptrArray[i];
}构造函数与友元
#include<bits/stdc++.h>
using namespace std;
/* 构造函数深入理解
1.cp20_1()=default表示让编译器生成一个默认构造函数
2.第二行冒号后面的内容表示为成员变量赋予形参的值,bookName(s)表示将该变量值设置为传入的实参s
3.构造函数后面冒号的内容叫做 构造函数初始值列表
4.如果像第三行中bookName()参数置空的话,表示用默认方法初始化bookName的值
*/
class cp20_1{
cp20_1()=default;
cp20_1(const string &s, unsigned b):bookName(s),age(b){}
cp20_1(unsigned c):bookName(),age(c){}
string bookName;
unsigned age;
};
// struct也可以替换类,但是它里面的所有成员都是public类型的
// class一般用来定义有private类型的成员
/* 友元
1.在一个类内对其他类或者函数使用friend声明友元函数
2.声明friend后,这些其他类和函数就可以使用本类中的private成员或其他成员
3.friend声明仅定义访问权限,如要使用外部类或函数,则需要再次普通声明一次
4.友元不能套娃,
*/
class cp20_2{
friend cp20_2 add(const std::string &s);
};
// 定义一个cp20_2接口的非成员组成部分声明
cp20_2 add(const std::string &s);
// 在cp20_3可以声明友元类cp20_4,表示对类cp20_3开放自己的所有内容
// 也可以只对专门的函数进行友元声明,那么操作权限仅对该类中的该函数有效
class cp20_4{
public:
void getOut();
};
void cp20_4::getOut(){}
class cp20_3{
friend class cp20_4;
friend void cp20_4::getOut();
};
/* 构造函数特性
1.构造函数可以有多个,也就是所谓重载
2.构造函数不能被const修饰
3.构造函数没有返回值!
*/可变成员与作用域
#include<bits/stdc++.h>
using namespace std;
// mutable关键词声明的成员叫可变数据成员
// 该成员可以被一const修饰的成员函数调用,但是他自己不可以被声明为const
class cp21_1{
public:
void getName() const;
private:
mutable size_t acc;
};
void cp21_1::getName() const{
acc++;
}
// 名字可以在不同作用域内覆盖,但是类型名不可以,所以不能在类里面再次using Money=xxx
// 类初始化是先声明成员函数然后再到函数体,所以最终return bal中的bal使用Money类型
using Money = double;
class cp21_2{
public:
Money balance(){return bal;}
private:
Money bal;
};委托构造函数
#include<bits/stdc++.h>
using namespace std;
// C++11新特性:委托构造函数
// 第二个构造函数叫委托构造函数,他委托给默认构造函数,调用默认构造函数执行赋值操作
// 运行构造函数时,先执行被委托对象,执行完后返回执行本构造函数的函数体
// 第三个构造函数先委托第二个,第二个再委托第一个,第一个执行完返回第三个构造函数函数体
class cp22_1{
public:
cp22_1(string name, int age, double health):name(name),age(age),health(health){}
cp22_1():cp22_1("",0,0.0){}
cp22_1(istream &is):cp22_1(){}
private:
string name;
int age;
double health;
};
// 类类型修饰,第一个加括号表示定义函数,第二个不加括号表示定义对象
cp22_1 cp22_1_1();
cp22_1 cp22_1_2;转换构造函数
#include<bits/stdc++.h>
using namespace std;
//---------------------------------------------------------------------
// 转换构造函数
// 当一个类构造函数仅一个形参时,可以使用类类型隐式转换机制,该构造函数叫转换构造函数
class cp22_2{
public:
cp22_2(string name){}
explicit cp22_2(int num){}
void getName(){}
};
// 使用直接初始化的方式初始化对象,传入string值,该值成为类构造函数的实参并实例化该对象
// 转换是必须是一步,不可以直接写cp22_2 ex1("abc"),因为系统执行两步转换操作,这是错的!
string name = "python";
cp22_2 ex1(name);
// 加了explicit修饰的构造函数则无法隐式实例化对应对象
// 所以该代码是错的:cp22_2 ex2(int(123))
// 但是显式使用构造函数则可以使用static_cast<...>来初始化
cp22_2 ex2(static_cast<cp22_2>(int(123)));
//---------------------------------------------------------------------聚合类&字面值常量&类内静态成员
#include<bits/stdc++.h>
using namespace std;
//---------------------------------------------------------------------
// 聚合类
// 形成条件:全部成员均public,无任何构造函数,无类内初始值,无基类
// 下面定义了一个简单的聚合类
class cp24_1{
public:
int a;
string b;
};
// 使用花括号对聚合类进行初始化,括号内值顺序必须和类中声明顺序一致
// 若对某些属性缺省,则自动初始化为默认值
cp24_1 ex1 = {1, "happiness"};
//---------------------------------------------------------------------
//---------------------------------------------------------------------
// 字面值常量类
// 1.情况一:数据类型都是字面值类型的聚合类可以视为字面值常量类
// 2.情况二:大多数情况下,把必须有一constexpr构造函数的,成员全是字面值常量的类叫做字面值常量类
// 3.在第二种情况下,构造函数初始化列表必须全部对数据成员初始化!
// 4.第二种情况下,constexpr构造函数不可以有函数体(初始化列表里面的变量也必须是字面值变量)
class cp24_2{
public:
constexpr cp24_2(bool a, bool b) : name(a),age(b){}
private:
bool name;
bool age;
};
//---------------------------------------------------------------------
//---------------------------------------------------------------------
// 类内静态成员
// 可以在类外部正常写入一个方法,在类内声明该方法时使用static把它变成静态的(不可在外部直接static)
// 类内静态成员不属于该类,所以类在初始化时不会对静态成员赋予初始值
// 实例化对象后用点运算符访问静态成员,或者直接用区域运算符并结合类名直接调用静态成员!!!
class cp24_3{
public:
static int stic_mth();
};
int cp24_3::stic_mth(){return 0;}
cp24_3 c24;
int a = c24.stic_mth();
int b = cp24_3::stic_mth();
// 静态成员可以作为类内函数的参数默认值出现,但是普通成员绝对不可以
// 静态成员的类型可以直接指定为本类的类类型,但是普通成员只能指定为本类的引用或者指针
// 当且仅当静态成员为字面值常量constexpr类型的,才可以在类内用一const整形数值初始化(就这一种情况!)
class cp24_4{
public:
cp24_4(char a=bg){}
private:
static const char bg;
static cp24_4 cp24;
static constexpr int period = 40;
};
//---------------------------------------------------------------------
int main(){
// 输出获取到的内容
cout<<a<<" "<<b<<endl;
return 0;
}类深入
继承机制
/*
派生类继承基类时,需要使用基类的构造函数来初始化基类中的成员
这一初始化过程也在派生类的构造函数中的初始化列表中书写
*/
class A{
public:
int a,b;
A(int x,int y):a(x),b(y){}
};
class B : public A{
public:
int c;
// 在列表初始化内使用基类的构造函数形式书写,注意该派生类构造函数接收三个实参,前俩个是留给基类的
B(int x,int y,int z):A(x,y),c(z){}
};
// D1为D2的直接基类,且D1为D3的间接基类,根据相隔的类做出称呼
class D1{};
class D2 : public D1{};
class D3 : public D2{};
// 类名称后加上final使得该类无法被继承
class CannotExtends final{};
/*
其他派生类内容
1. 可以将基类的指针或者引用绑定到派生类对象上
2. 基类自动转换成派生类是不存在的
3. 派生类向基类的自动类型转换只对指针或者引用类型有效
4. 派生类向基类转换依然受到访问等级限制
*//*
虚函数继承规定
1. 一旦某个函数被定义为虚函数,那么所有派生类中他都是虚函数
2. 派生类函数可以覆盖继承的虚函数,但是形参数量类型以及返回值类型都不可以改变!
3. 派生类中使用override覆写而不是virtual,编译器会有自动纠错的功能
务必原封不动的覆写继承!如下方那个代码,函数后面的const也被搬了下来
*/
class A {
virtual void a1(int k) const;
};
class B : public A {
void a1(int k) const override;
};虚函数
// 记住一个法则:只要存在继承关系,基类内通常会定义一个虚析构函数
class Quote{
public:
Quote() = default;
virtual ~Quote();
};
/*
虚函数法则
1. 为一个成员函数添加virtual关键字可以吧该函数变成一个虚函数
2. 基类中函数一旦被定义为虚函数,暗示派生类也需要对该虚函数执行覆写override
3. 基类中的虚函数在派生类中隐式为虚函数
4. virtual只能出现在类内部声明语句之前而不能用于外部函数定义
5. 未被声明为虚函数的函数,他的解析过程发生在编译时而非运行时
*/
/*
访问控制与继承
派生类可以访问基类Public域但是无法访问private域
派生类也可以访问protected域但是外界无法访问基类中protected域中的内容
*/
/*
派生类中的虚函数
1. 派生类可以不覆盖基类中的虚函数,那么将执行默认继承
2. 在欲覆盖的函数前以virtual修饰
3. 不使用virtual来执行覆盖,则需要再以下几个位置的后面加上override(C++11特性)
形参列表后面;
const成员函数的const关键字后面;
引用成员函数的引用限定符后面;
*/class Base {
public:
// final禁止虚函数被派生类继续覆写!
virtual void show() final {
std::cout << "Base show" << std::endl;
}
};纯虚函数
/*
纯虚函数定义
1. 该函数首先要被virtual修饰变成虚函数,才可以进一步变成纯虚函数
2. 在虚函数末尾分号前加 =0 就可以变成纯虚函数
3. 纯虚函数在类内时,不可以提供定义(即不能写函数体)
纯虚函数在类外时才可以定义函数体
*/
class A{
public:
virtual double pure_virtual(int) const = 0;
};
/*
抽象基类
1. 一个类中定义了纯虚函数,那他就是抽象基类
2. 抽象基类无法直接实例化(就如JAVA中的抽象类无法实例化)
3. 必须在抽象基类的派生类中实现纯虚函数后,才可以对派生类执行实例化
4. (我们暂且可把cpp中的纯虚函数看成java中的抽象方法,而抽象基类就相当于抽象类)
*/
class B{
public:
virtual int getInt(int x) const =0;
};
class C : public B{
public:
int getInt(int x) const override;
};
void instance(){
C c(); // 派生类覆盖了纯虚函数,所以可以实例化
}派生类
/*
访问控制与继承(以下面一个比较复杂的例子作为解析)
对于派生类函数体内
无法访问基类中的保护型成员protected
对于派生类的成员或者友元
1. 他可以访问派生类函数体内的protected和private成员
2. 他可以通过派生类对象来访问其基类中的受保护成员(主义这个通过!!!)
3. 他不可以直接使用基类对象的受保护成员
*/
class Base {
protected:
int num;
};
class Son: public Base {
friend void clonable(Son&);
int j; // j默认是私有的private
};
void clonable(Son& s) {
cout << s.num;
}
/*
派生类向基类转换的可访问性质
1. 若B公有继承A,才可以将派生类B的对象转换为基类对象A
如果B私有或者保护继承A,则以上操作无效
2. 无论B以何种方式继承自A,派生类B中的成员或友元都可以任意的对基类执行访问
*/
/*
派生类作用域位于基类作用域内
派生类使用方法是依据名字逐层向上查找的!
假设类ABC连续继承,ABC都有方法m1,但只有BC有方法m2,只有A有方法m3
1. 若C的对象调用方法m3,他自己没有,找到父类B发现也没有,B在找到父类A最终找到
所以调用A中的方法m3,这就是逐层向上按名字查找原则
2. 若A的对象调用方法m2,因为他是顶层类所以没有父类(所以无法执行向上查找)
而他自己也没有该方法,故错误!
*/
/*
除了虚函数之外,不要在派生类中为方法取一个和基类中某方法一致的名字!
普通方法会在派生类中因为重名而被覆盖掉,即使用时只能使用派生类的
*/
/*
派生类正确继承基类中虚函数
保证派生类覆写时,返回值、形参数量、形参类型都是一直不变的!!!
虚函数内部写什么随意
*//*
派生类与虚析构函数
1. 最好定义基类的析构函数为虚析构函数,使得他可以动态绑定
2. 因为当我们delete派生类时,如果基类仅为普通析构函数,那么执行的可能及时基类的析构函数
而非派生类的虚构函数,造成内存删除错误
*/
class A1 {
virtual ~A1() = default;
};
class A2: A1 {
virtual ~A2() = default;
};
/*
派生类与基类的拷贝控制函数之间的关系
1. 若基类中的拷贝控制成员之一被设置为删除的或不可访问的,则派生类对应的成员也是别删除的
2. 基类中若有一不可访问或删除的析构函数,则派生类中移动构造函数也被定义为删除
*/
/*
移动操作与继承
为了解决因为基类定义了删除的或无法访问的析构函数而导致派生类没有移动构造函数
使用以下方法依次为派生类构造对应的函数
*/
class Demo {
public:
Demo() = default; // 对成员依次进行默认初始化
Demo(const Demo&) = default; // 对成员依次拷贝
Demo(Demo&&) = default; // 对成员依次拷贝
Demo& operator=(const Demo&) = default; // 拷贝赋值
Demo& operator=(Demo&&) = default; // 移动赋值
virtual ~Demo() = default;
};运算符重载
void or1(){
/*
把运算符定义为成员函数后,他的左侧运算对象必须是运算符所属类的一个对象
*/
string str = "world!";
string t = str+"i"; // 右侧const char*,而左侧string归属于右侧的类,故可以相加
string k = "asd"+str; // 右侧string不是左侧const char*所属的类,所以错误
}
/*
运算符函数的基本概念
1. 不可以重载一个基本类型的运算符函数,如下方法是错误的
int operator+(int, int)
2. 我们只能重载现存的符号,无法自创符号
3. 有些符号是无法重载的,譬如三元运算符和冒号运算符等少部分
4. 运算符重载具有对称性,譬如你使用operator==后必须要重载他的对称运算operator!=
赋值和复合赋值运算符
1. 重载运算符应当符合而非违背其内置含义,譬如operator+=就有先加再赋值的含义,不可颠倒
2. 运算符定义需要考虑他是否为成员函数还是普通非成员函数
== [] () -> 必须是成员函数
复合赋值运算符一般为成员函数,但需要根据具体情况而定
具有对称性(==和!=)的运算符应当作为普通非成员函数
3. 一般吧普通运算的(+-*)定义为成员函数,但唯一的要求是至少有一个成员是类类型的
*//*
重载输入输出运算符的共有特性
1. 假设输入输出运算符是某个类的成员,那么这两个运算符也必须是istream或ostream的成员!
2. 我们无法给标准库的类(如istream)添加任何成员
3. 无论输入输出运算符,都必须声明为非成员函数,他们同时还必须是友元!
*/
class Test {
public:
// 列表初始化执行成员变量初始化
Test(int x, int y): x(x), y(y) {}
// 声明重载输入输出运算符为友元
friend ostream &operator<<(ostream &os, const Test &t);
friend istream &operator>>(istream &is, Test &t);
private:
int x, y;
};
/*
重载输出运算符
1. 参一为非常量ostream对象引用
2. 参二为常量的引用,为了避免不必要的复制
3. 使用os接收内容,然后再把os给return回去,即完成重载的全步骤
4. 输出运算符最好避免使用格式化操作
*/
ostream &operator<<(ostream &os, const Test &t) {
os << t.x << "这是一行测试文本" << t.y;
return os;
}
/*
重载输入运算符
1. 参一为非常量istream对象引用
2. 参二为读入对象的引用,因为数据会变化所以不能声明为常量
3. 输入运算符必须处理失败的状况(如下代码),而输出运算符不需要
*/
istream &operator>>(istream &is, Test &t) {
is >> t.x >> t.y;
// 判断is是否输入成功,如果成功则执行后端的代码
// 如果输入失败,则为对象赋予初始化状态后反馈回去(注意初始化别漏了填入参数!)
if (is) t.x += t.y;
else t = Test(0, 0);
return is;
}
int main() {
Test t1(10, 20);
cin>>t1;
cout << t1 << endl;
return 0;
}/*
算数运算符
1. 定义为非成员函数
2. 当你同时定义了复合赋值运算符和其对应的算术运算符,那么在算术运算符重载中
应当使用复合赋值运算符来执行运算操作
*/
/*
相等运算符
1. 定义为非成员函数
2. 一旦重载了相等运算符operator==那么必须要重载operator!=
3. 除非两个对象具有可以直接比较是否相等的能力,否则别重载这俩运算符
*/
class Point {
private:
int x, y;
public:
// 构造函数
Point(int px, int py) : x(px), y(py) {}
// 声明友元函数
friend bool operator==(const Point&, const Point&);
friend bool operator!=(const Point&, const Point&);
};
// 重载'=='运算符
bool operator==(const Point& p1, const Point& p2) {
return p1.x == p2.x && p1.y == p2.y;
}
// 重载'!='运算符
bool operator!=(const Point& p1, const Point& p2) {
return !(p1 == p2); // 调用重载后的'=='运算符
}
int main() {
Point p1(1, 2);
Point p2(1, 2);
Point p3(3, 4);
std::cout << "p1 == p2: " << (p1 == p2 ? "true" : "false") << std::endl; // 应该输出 true
std::cout << "p1 != p2: " << (p1 != p2 ? "true" : "false") << std::endl; // 应该输出 false
std::cout << "p1 == p3: " << (p1 == p3 ? "true" : "false") << std::endl; // 应该输出 false
return 0;
}class ForInt{
public:
ForInt(){}
ForInt &operator=(int num);
private:
int x;
};
/*
重载相等运算符
1. 他必须被定义为成员函数,注意提前声明
2. 函数返回左侧运算对象的引用,如return *this;
复合赋值运算符
他不一定必须定义为成员函数,但同样的该复合赋值运算符也应该返回其左侧运算对象引用
*/
ForInt &ForInt::operator=(int num){
x = num;
return *this;
}
/*
下标运算符
1. 必须是成员函数
2. 以所访问元素的引用作为返回值
3. 最好同时定义下标运算符的常量和非常量版本
*/
class Noob{
public:
Noob(string s):str(&s){}
// 非常量版本
string &operator[](size_t n){
return str[n];
}
// 常量版本,注意一前一后俩const
const string &operator[](size_t n) const{
return str[n];
}
private:
// 因为下标运算符返回的是引用,所以必须这样定义
string *str;
};class I{
public:
int pos;
/*
前置递增运算符
1. 返回递增或者递减后对象的引用
2. 最好定义为成员函数
3. 前置递增原则就是先自增再赋值,以下也严格按照此步骤进行(return *this就是赋值过程)
*/
I &operator++(){
pos++;
return *this;
}
/*
后置递增运算符
1. 他需要标记一个无名称形参,因为这里递增的是int类型值,所以形参里面只用写int即可
2. 按照后置递增原则,我们先取得递增前对象引用,待对象递增完毕后返回原即可遵照原则
3. 返回值是一个对象而非对象的引用!
4. int形参仅作为区分前后置递增递减运算符而存在,实际操作不会使用他们
*/
I &operator++(int){
I temp = *this;
pos++;
return temp;
}
};
/*
以下代码表示了显式调用一个后置递增运算符,注意他传入了一个任意的参数作为int形参的实参输入
(这里实参传值0,事实上你写入任何一个不大于int范围的值都可以)
p.operator++(0);
*/重载调用
/*
成员访问控制符
1. 解引用运算符和箭头运算符通常是成对出现的,这体现在智能指针上面
2. 以上两种运算符重载通常都是类成员
3. 重载运算符函数必须返回类的指针,或者自定义了箭头运算符的某个类的对象!
*/
class Obj{
public:
int x,y;
Obj(int a,int b):x(a),y(b){}
void say() const{
cout<<"helloworld"<<x<<y;
}
friend class SmartPtr;
};
class SmartPtr{
public:
Obj o;
SmartPtr(Obj &oc):o(oc){}
Obj *operator->() const{
return (Obj*)0;
}
};/*
最简单的函数调用运算符重载例子
1. 他可以让一个类的对象像一个方法一样使用,提升对象的容错率
2. 若定义了调用运算符operator(),那么该类的对象称为 函数对象
下面展示了制作一个取绝对值的类,之后重载调用运算符
可见我们对对象ai直接进行”类似方法一般”的调用操作
*/
struct absInt {
int operator()(int val) const {
return val < 0 ? -val : val;
}
};
void getAbs() {
int i = -2;
absInt ai;
int j = ai(i);
}
/*
含有状态的函数对象类(以下方代码为例)
假设我们需要制作一个函数对象类,使得可以传入字符串并按照规定格式输出
1. 构造函数接收一个输出流以及间隔字符,列表初始化
2. 重载调用运算符,接收欲输出的字符串s,然后将其按照约定的格式塞入输出流内
3. 因为我们已经定义了输出流为cout,所以不需要显示调用如 cout << os
*/
class PrintStr {
public:
PrintStr (ostream &o = cout, char c = ' '): os(o), sep(c) {}
void operator()(const string &s) const {
os << s << sep;
}
private:
ostream &os;
char sep;
};
void PrintString() {
// str为我们需要输出的字符串
// 因为类PrintStr的构造函数形参已经有初始值,所以下面的psr变量忽略传参而直接构造
// 调用函数对象psr(str)来按照格式输出内容
string str = "helloworld";
PrintStr psr;
psr(str);
// 这里通过对构造函数传入实参,改变了输出方式以及间隔字符sep
// 然后照例调用函数对象来输出内容
PrintStr psr_error(cerr, '\n');
psr_error(str);
}
/*
lambda是函数对象
这句话的意思是,当我们使用一个lambda函数时,他就可以类似于一个对应的类
在下方的shortStr方法中我们使用lambda执行了按照单词长度进行排序的谓词
那么该方法下方的ShortStr就是该lambda函数对应解析的类
*/
void shortStr(){
// 一条又臭又长的lambda函数
vector<string> str = {"hello","hey","hi"};
stable_sort(str.begin(),str.end(),
[](const string &s1,const string &s2){return s1.size()<s2.size();});
}
// lambda函数对应的类实现,该类仅包含一个调用运算符重载
class ShortStr{
public:
bool operator()(const string &s1,const string &s2) const{
return s1.size() < s2.size();
}
};/*
标准库定义一系列函数对象,让我们可以依据对应模板来执行运算操作
如下plus<int>表示这是一个相加的函数对象,且接受的数据类型为int
*/
void stl1(){
// plus<T> 执行俩数据之和的功能
plus<int> ip;
int k = ip(10,20);
// negate<T> 执行取反功能
negate<int> in;
int q = in(10);
}
/*
标准函数库还规定了对应的关系运算,例如上一节我们使用lambda函数实现的比较元素大小的谓词
在这里则直接可以使用模板函数对象来实现
下方代码为sort传入了greater<string>函数对象,他进行>比较,故sort结果为降序排列
标准库函数对象的作用就可以替换我们手写的谓词了!
特别注意greater<string>() 后面有小括号,因为他是函数对象!
*/
void stl2(){
vector<string> str{"123","asdqw","fasdqw"};
sort(str.begin(),str.end(),greater<string>());
}/*
可调用的对象
1. 首先定义欲被调用的方法add
2. 在方法useAdd内,map的key接收字符串,而value则接收一接收俩int实参且返回int值的方法!
3. 在我们对该哈希表插入值时,他的value应该就是我们写的方法add
4. 下面展示俩方法插入,一个显式调用pair而另一个隐式调用pair,二者效果是一致的
*/
int add(int a,int b){
return a+b;
}
void useAdd(){
map<string,int(*)(int,int)> binops;
binops.insert(pair<string,int(*)(int,int)>("hello",add)); // pair插入法
binops.insert({"hey",add}); // 隐式pair插入
}
/*
标准库function类型
1. 它类似于给函数取别名,代为该函数使用
2. 他接受模板参数,该参数即为函数的完整声明
*/
void useFunction(){
function<int(int,int)> f1 = add; // 使用function
cout<<f1(1,2)<<endl; // 输出3
// 因此我们可以把上一个可调用对象的map定义更换为
map<string,function<int(int,int)>> m;
}
/*
使用function来完成加减乘除配置
这里直接对哈希表进行初始化
1. {"+",add}调用弹出初始化
2. {"-",std::minus<int>()} 调用标准库函数对象初始化
3. {"*",[](int a,int b){return a*b;}} lambda函数当然也可以完成此操作!
*/
void stl3(){
map<string,function<int(int,int)>> binops = {
{"+",add},
{"-",std::minus<int>()},
{"*",[](int a,int b){return a*b;}}
};
}/*
类型转换运算符规则
1. 他必须定义为类内成员函数,负责将类类型的值转换成其他类型
2. 类型转换运算符可以面向任何类型(除了void)
3. 不允许转换成数组、函数类型,但可以转换成指针、引用类型
类型转换运算符格式:
operator type() const;
下面的例子展示了当我们再执行某项运算时,如果将对象带入,则会启动该对象的类型转换运算符
当且仅当我们重写了这个玩意才可以生效,不然会报错
*/
class SimpleGame {
public:
SimpleGame(int val): x(val) {}
operator int() const {
return x;
}
private:
int x;
};
void transit() {
SimpleGame sg(10); // 调用构造函数为x赋值
int num = sg + 10; // 调用类型转换运算符,使得对象sg返回int类型的值x
cout << num << endl; // 输出20
}
/*
显式的类型转换运算符
只需要为类型转换运算符前面加上explicit就好了
那么当该类的对象使用类型转换运算符时必须是显式的,而不能是隐式的
即必须要使用static_cast<type>()来执行调用
*/
class OtherGame{
public:
OtherGame(int val):x(val){}
explicit operator int() const{
return x;
}
private:
int x;
};
void geto(){
OtherGame og(100);
// 显式调用类型转换运算符才可以使用
int oog = static_cast<int>(og) + 100;
}数据结构
cerr&clog
#include<bits/stdc++.h>
using namespace std;
void outp(){
// cerr和clog分别输出错误信息和一般性信息
cerr << "这是一条标准的错误信息" << endl;
clog << "这是一条普通的信息" << endl;
}
void cp1_2(){
// 当cin读取到一文档结束符或错误字符时就会停止输入,以此方法读取变长度内容
// 但此方法并不实用,因为每一个数字都必须空格分开且最后按enter可能也无反应
int sum=0,value=0;
while(cin >>value){
sum += value;
}
cout << "求和值为:" << sum << endl;
}引用与地址
void cp2_1(){
bool b1 = (bool)100; //非bool值赋bool,若非0则bool值为true,反之若为0则bool值为false
}
void cp2_2(){
// cout里面字符串字面量太长了直接换行也是OK的
cout << "this is a \\n sign that "
"can be change" << endl;
}
int i; // 定义于函数体外的变量不初始化也会赋予初始值
void cp2_3(){
int ii=0; //函数体内变量必须初始化,否则它会变成未定义的
}
void cp2_4(){
// 引用可以理解为为某变量取了别名
// 一个引用只能指向一个目标,也就是说不能指向一个引用的引用
// 引用不是对象,譬如c可以完美代替a使用
int a=1,b=2;
int &c = a;
printf("%d",c);
}
void cp2_5(){
// 在c++中,int叫做数据类型,而*和&叫做类型修饰符
// 一个类型修饰符只能作用于一个变量,所以p2是int类型而非int*类型!!!
int *p1,p2;
int *p3,*p4;
}
void cp2_6(){
// 说白了,对指针的引用就是对指针取了个别名而已,和普通的&作用是一样的
int i=42;
int *p;
int *&r = p; //从右往左读,定义一个r引用,且其为一个指针
r = &i; //因为r指向指针p,所以翻译过来就是p=&i,也就是让指针p指向变量i
*r = 0; //同理,翻译过来*p=0,也就是把p指向的变量i的值变成0
}常量与使用规范
#include<bits/stdc++.h>
using namespace std;
// 声明常变量必须要初始化不然报错
const int a = 100;
// 由于常量默认只在一个文件中生效,而加了extern则可以在多个文件中引用
extern const int b = 100;
void cp3_1(){
// const int &b表示对常量的引用
// 对常量的引用应该指向常量,且非常量引用不可以指向一个常量!
const int a = 10;
const int &b = a;
// 对常量的引用仍然可以指向一个非常量,但是不允许通过ii来改变i
// 我们可以直接改变i的值从而间接改变ii指向的值,这是合法的
int i=10;
const int &ii=i;
}
void cp3_2(){
// 要存放常量对象的地址就必须用指向常量的指针
// 普通指针无法指向一个常量
const double i=12.33;
const double *ii = &i;
// 事实上,指向常量的指针也可以指向非常量,语法正确
}
void cp3_3(){
// 常量指针的作用就是无法修改他的值
int outter=0;
int *const k = &outter;
// 声明一个指向常量的常量指针(指向改不了,内容也改不了)
const int *const ll = &outter;
}
void cp3_4(){
// constexpr关键词修饰后,改变量就变成了一个常量
// 该关键词就是为了让编译器验证变量的值是否是一个常量表达式
constexpr int mf = 100;
}
int main(){
return 0;
}
/* 写在后面
我们把指向常量的指针称为顶层const,把常量指针称为底层const
*/typedef & decltype
#include<bits/stdc++.h>
using namespace std;
void cp4_1(){
// cpp11引入了别名声明,也就是using那玩意,他和typedef等价替换
typedef double DB;
using DBB = double;
// cpp11新增的auto可以自动推演数据类型,但别用在顶层const和底层const的命名上
auto a = 100;
}
int foo(){return 0;}
void cp4_2(){
// decltype可以获取并返回某函数的返回值类型,但是并不调用该函数
// 注意这里使用了外部函数,不然本函数内无法识别
extern int foo();
decltype(foo()) num = 1234;
// decltype函数还可识别一个变量亦或是一个表达式的最终数据类型
// 如i+100得出结果类型为int
// 而ii是一个引用类型,也就是int&,所以这一行代码必须初始化!!!
int i=0,&ii=i;
decltype(i+100) p1;
decltype(ii) p2 = p1;
// 特别的,当再用一个括号把变量括起来时,变量含义立刻转变为引用,所以这里类型为int&
int a1=0,&a2=a1;
decltype((a1)) a3 = a1;
}
int main(){
return 0;
}字符串操作与 auto 修饰符
#include<bits/stdc++.h>
using namespace std;
// string初始化术语
string s1;
string s2(s1); // s2是s1的一个副本
string s3("hello"); // s3是字符串字面量hello的副本,他的含义和下面一样
string s4="hello";
void cp5_1(){
string s1="helloworld"; // 拷贝初始化
string s2("helloworld");// 直接初始化
string s3(10,'c'); // 直接初始化,内容为十个c,也就是cccccccccc
}
void cp5_2(){
string s1;
cin >> s1; // 读取字符串,忽略开头所有空格并在读入字符后遇到的第一个空白结束
}
void cp5_3(){
string s1;
while(getline(cin,s1)){ // getline读取一行字符,cin表示输入,s1为存储字符的变量
cout << s1 << endl;
}
if(s1.empty()) cout << "null"; // empty检查是否为空
if(s1.size()!=0) cout << s1; // size返回长度
size_t st = s1.size(); // size返回一个size_t的无符号整数,故如此存储
}
void cp5_4(){
string s1="hello";
string s2="helloworld";
string s3="hey";
if(s2>s1) cout << "yes!" << endl; // 若俩string开头一样且一个较短,则较短的一方偏小
if(s3>s2) cout << "not" << endl; // 此时比较的是第一个不相等字符的ASCII码谁的更大
}
void cp5_5(){
string s1="hey",s2="tom";
string s3=s1+"john"; // 俩子串相加时,字符串字面量左右至少有一个变量
// 譬如"hello"+"tom"错误,因为两者左右都没有一个string变量
string s4=(s1+"not")+"think"; // 完美解决问题
}
// cctype头文件函数简析(需要提前导入,这里用了万能头文件)
void cp5_6(){
cout << isalnum('a') << endl; // 是否为数字或字母
cout << isalpha('1') << endl; // 是否为字母
cout << isdigit('3') << endl; // 是否为数字
cout << isspace(' ') << endl; // 是否空白
cout << islower('a') << isupper('B') << endl; // 判断大小写字母
cout << ispunct('.') << endl; // 是否为标点符号
}
void cp5_7(){
// cpp11新出炉foreach方法,遍历str内的每一个字符并赋给c,并输出c
string str("helloworld");
for(auto c:str){
cout << c << "\t";
}
cout << endl;
// 遍历改变字符串内容,需要使用引用!
for(auto &c:str){
c=toupper(c);
cout << c << "\t";
}
cout << endl;
}vector
#include<bits/stdc++.h>
#include<vector> //使用vector必备的头文件
using namespace std;
// vector也成为容器,它存储对象,但vector本身不是一个类型,且不存在vector的引用
vector<double> cp1; //定义一个vector,其中存储的元素都是double类型的
vector<vector<int> > cp2; //vector进行类型嵌套时注意最后尖括号的空格,老编译器若无空格会报错
vector<int> cp3_1;
vector<int> cp3_2 = cp3_1; //俩类型相同的vector可以直接进行副本拷贝
vector<string> s1{"abc","hello","boy"}; //列表初始化,用的花括号且无等号
vector<string> s2(10,"hey"); //初始化10个具有相同hey值的string对象
vector<double> d1(10); //若vector使用内置类型,则允许不初始化而直接给定对象数目
vector<double> d2{10}; //上面表示创建10对象,而这里是创建一个值为10的对象,注意区分!
void cp6_1(){
// 向一个string类型的vector中底部不断插入元素(类似于在栈顶插入)
string word;
vector<string> str;
while(cin >> word){
str.push_back(word);
}
}
void cp6_2(){
vector<unsigned> num(11,0); //将十一个无符号整数对象都赋予初始值0
cout << num[1] << endl; //可以直接通过下标访问vector内存储对象的值
//请勿试图用num[1]=10来赋予元素,这是错的,只能用push_back()函数
}迭代器
#include<bits/stdc++.h>
using namespace std;
void cp7_1(){
// 使用begin来获取字符串指向的第一个元素的迭代器
// 因为迭代器类型未知,直接auto让系统自动判断迭代器类型
// 解引用迭代器可以获取其指向的值
string s("helloworl");
auto it = s.begin();
cout << *it <<endl;
}
void cp7_2(){
// s1.end()获取最后一个字符的后一位的迭代器,所以*(s1-1)=y
// 当字符串是空的,则begin和end返回同一迭代器
string s1("some body");
if(s1.begin()!=s1.end()){ //确保s1非空
auto it = s1.begin(); //将it指向迭代器的第一个字符
*it = toupper(*it); //将当前字符改写成大写的形式
}
cout << *(s1.end()-1) <<endl;
}
// 迭代器类型
void cp7_3(){
// 对于一个非常量,他的迭代器类型格式:数据类型::iterator
// 该迭代器::iterator可以对内容读写
string s1("hello");
string::iterator si1 = s1.begin();
// 仅可对常量使用const_iterator类型,此迭代器仅可读不可写
// 注意向量不是一个类型,所以要写vector<string>
vector<string> s2(10,"str");
vector<string>::const_iterator sci1 = s2.begin();
}
void cp7_4(){
vector<int> v1{1,39,221};
auto v2 = v1.cbegin(); // cbegin()和cend()函数取出的迭代器都是const_iterator类型的
vector<int>::const_iterator v3 = v1.cend();
}
void cp7_5(){
// (*s2).empty()检测迭代器指向的内容是否为空,注意括号包住解引用
// 可以使用箭头表达式来代替指针,如(s2+1)->empty()
vector<string> s1{"hey"};
vector<string>::iterator s2 = s1.begin();
if(!(*s2).empty() && !(s2+1)->empty()){
cout << "got it!" <<endl;
}
}
void cp7_6(){
// 对迭代器使用加法表示返回值前移(但以我们视角事实上是指向迭代器后移)
// 注意s2+1表示指向hey,而不是在迭代器中的一个对象进行操作!!!
vector<string> s1{"helloworld","hey","thanks","tombs"};
auto s2 = s1.begin();
cout << *s2 << *(s2+1) <<endl;
// 俩迭代器相减返回顺序差,类型为difference_type,这里用auto判断
auto s3 = (s2+3)-s2;
}
// 迭代器实现二分查找!!!
void important(){
vector<int> text(10,0);
auto beg = text.begin(), end = text.end();
auto mid = text.begin() + (end - beg)/2; // 初始状态下中间点
}数组迭代器与引用
#include<iostream>
#include<cstddef>
using namespace std;
void cp8_1(){
int c0[10];
int *c1[10]; // 数组指针,含10个整形指针
int (*c2)[10]=&c0; // 指向一个含有10个整数的数组的指针
int (&c3)[10]=c0; // 引用一个含有10个整数的数组
}
void cp8_2(){
// 使用范围遍历的方法取出多维数组所有元素(按照顺序),这里不需要我们手动控制范围
int a[3][2]={{3,2},{0,9},{34,213}};
for(auto i:a){
cout<<a<<" ";
}
cout << endl;
}
void cp8_3(){
// 指针数组可也可以使用迭代器操作
// 让指针指向一不存在的索引(也就是最后一个元素的后一位),获取数组c1的地址用于初始化
// 循环解释:让指针b指向数组c1,每次循环指向后移一位,移动到c2(不存在的索引)位置后停下
// 这种方法十分不安全,建议使用cp8_4内的begin和end做法
int c1[4]={1,2,3,4};
int *c2 = &c1[4];
for(int *b=c1;b!=c2;++b){
cout << *b << endl; // 借b之手输出c1元素
}
}
void cp8_4(){
int c1[4]={1,2,3,4};
int *c2 = begin(c1); // 指向数组c1首元素的指针
int *c3 = end(c1); // 指向数组c1最后一个元素的下一位的指针
}
void cp8_5(){
// size_t和ptrdiff_t类型都存在头文件cstddet.c内
constexpr size_t sz = 5; // 数组中元素个数类型的size_t
constexpr ptrdiff_t pt = 4; // 俩数组指针相减结果为ptrdiff_t类型
}
void cp8_6(){
// 又一种遍历数组元素的方法,c指向数组最后一元素下一位,用作与b一起的判据
double a[3]={3.44,2.239,9.541};
double *b=a,*c=a+3;
while(b<c){
cout << *b++ << " " ;
}
cout<<endl;
}
void cp8_7(){
// 指针还可以这么玩,p指向a[3]也就是第三个元素,故p[-2]就指向了a的第一个元素
int a[5]={1,2,3,4,5};
int *p = &a[2];
int k = p[-2]; // 此刻的p[-2]取出的是一个值而不是指针,故k不可以使用指针形式
}其余字符串杂项
void cp9_1(){
// 两个string类型可以进行大小比较,但是两个字符数组无法进行比较,因为他们比较的是指针
string s1="helloworld";
string s2="hey";
if(s1>s2) cout<<"yes"<<endl;
// 这是C风格的字符串
char c[]={'a','b','\0'}; // 逐个初始化字符数组必须要在最后加一个\0
}
void cp9_2(){
string s1="nothing";
const char *str=s1.c_str(); // c_str把string类型转换成一个指针,此时才可用string初始化char*
// 使用数组的开头和结尾初始化一个int向量(类似于拷贝操作)
int num[]={1,2,3,4,5,6};
vector<int> num2(begin(num),end(num));
}
原生 C 多线程
一文搞定之 C 语言多线程_c 语言线程-CSDN 博客 > C 语言中的多线程编程如何实现?_c 多线程编程-CSDN 博客
POSIX 实现入门
C 语言没有 CPP 提供的 thread 多线程库,此时需要借助 POSIX 的 pthread 来实现多线程;
入门多线程 下面给出了一个简单的多线程例子
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
// 使用线程时需要添加<pthread.h>这个头文件
// myfunc 线程携带的函数;固定为Void*型
void myfunc(void *args)
{
printf("hello world\n");
return;
}
int main()
{
// 声明线程th1
pthread_t th1;
pthread_create(&th1, NULL, (void *)myfunc, NULL);
pthread_join(th1, NULL);
return 0;
}多线程加锁 如果不使用线程互斥锁的话,就会出现多个线程同时修改一个数据出现数据不一致的情况;
- 线程加互斥锁 pthread_mutex_t,开启锁后设置临界区,其他线程不可以干扰临界区代码的执行;
- pthread_mutex_unlock 退出临界区,释放锁,其他线程即可继续抢占该锁
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
long s = 0;
pthread_mutex_t lock;
void *func(void *)
{
pthread_mutex_lock(&lock);
int i;
for (i = 0; i < 10000; i++)
s++;
pthread_mutex_unlock(&lock);
return NULL;
}
int main(void)
{
pthread_t th1, th2;
pthread_create(&th1, NULL, func, "th1");
pthread_create(&th2, NULL, func, "th2");
pthread_join(th1, NULL);
pthread_join(th2, NULL);
printf("%ld \r\n", s);
return 0;
}动态内存分配与线程退出
函数执行流程:创建一个线程,并把线程 ID 作为参数传递进去;线程函数内部使用 sleep 模拟耗时任务,最后将线程 IDx2 后封装到 pthread_exit 后退出线程函数;主线程校验线程函数返回值,并打印;
- pthreadcreate(&th1, NULL, func, (void )&ptid),最后一个参数为传入线程函数的参数,因为线程函数的参数是一个 void(表示任意数据类型指针),所以我们需要先取地址后直接进行指针转换即可
- int _res = (int _)malloc(sizeof(int)); 返回值指针需要进行 malloc 内存分配
- 最后使用 free(res);对分配的内存进行释放,否则会导致内存泄漏问题!
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void *func(void *args)
{
int thread_id = *(int *)args;
printf("thread %d is running \r\n", thread_id);
for (int i = 0; i < 10; i++)
{
printf("thread is building %d \r\n", i);
sleep(1);
}
int *res = (int *)malloc(sizeof(int));
*res = thread_id * 2;
printf("thread done! \r\n");
pthread_exit(res);
}
int main(void)
{
pthread_t th1;
int pt_id = 1;
if (pthread_create(&th1, NULL, func, (void *)&pt_id) != 0)
{
fprintf(stderr, "failed to print \n");
return 1;
}
void *res;
if (pthread_join(th1, &res) != 0)
{
fprintf(stderr, "child thread is finished \n");
return 1;
}
printf("all threads is done %d \n", *(int *)res);
free(res);
return 0;
}分离线程
pthread_detach 开启一个分离线程,该线程执行完毕后自动回收资源,而普通的线程需要先 create 然后调用 join 才可以回收资源;
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* thread_function(void* arg) {
printf("Thread started.\n");
// 模拟线程工作
sleep(3);
printf("Thread finished.\n");
return NULL;
}
int main() {
pthread_t thread_id;
// 创建新线程
if (pthread_create(&thread_id, NULL, thread_function, NULL) != 0) {
perror("pthread_create");
return 1;
}
// 分离线程
if (pthread_detach(thread_id) != 0) {
perror("pthread_detach");
return 1;
}
sleep(4); // 等待足够长的时间以观察线程的工作
printf("Main thread finished.\n");
return 0;
}互斥锁 mutex
pthread_mutex_init:初始化互斥锁,将互斥锁设置为可用状态。 pthread_mutex_lock:加锁,尝试获取互斥锁,如果互斥锁已经被其他线程占用,则调用线程会被阻塞,直到互斥锁可用。 pthread_mutex_unlock:解锁,释放互斥锁,让其他线程可以获取互斥锁。 pthread_mutex_destroy:销毁互斥锁,释放相关资源。
#include <stdio.h>
#include <pthread.h>
#include <stdint.h> // 引入 intptr_t 类型
int counter = 0; // 共享的计数器
pthread_mutex_t mutex; // 互斥锁
void* increment_counter(void* thread_id) {
intptr_t tid = (intptr_t)thread_id; // 使用 intptr_t 来存储指针值
pthread_mutex_lock(&mutex); // 加锁
counter++; // 计数器增加
printf("Thread %ld: Counter value is %d\n", tid, counter);
pthread_mutex_unlock(&mutex); // 解锁
pthread_exit(NULL);
}
int main() {
pthread_t threads[5]; // 5个线程
pthread_mutex_init(&mutex, NULL); // 初始化互斥锁
for (intptr_t i = 0; i < 5; i++) {
pthread_create(&threads[i], NULL, increment_counter, (void*)i); // 创建线程,并传递线程编号
}
for (int i = 0; i < 5; i++) {
pthread_join(threads[i], NULL); // 等待线程结束
}
pthread_mutex_destroy(&mutex); // 销毁互斥锁
return 0;
}信号量 semaphore
sem_init:初始化信号量,设置信号量的初始值。 sem_wait:等待信号量,如果信号量的值大于 0,则将信号量的值减 1 并继续执行;如果信号量的值为 0,则等待直到有其他线程释放信号量。 sem_post:释放信号量,将信号量的值加 1,唤醒一个或多个等待的线程。 sem_destroy:销毁信号量,释放相关资源。
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdint.h> // 引入 intptr_t 类型
#define NUM_THREADS 5
int counter = 0;
sem_t semaphore;
void* increment_counter(void* thread_id) {
intptr_t tid = (intptr_t)thread_id;
// 等待信号量
sem_wait(&semaphore);
printf("Thread %ld: Incrementing counter.\n", tid);
counter++;
printf("Thread %ld: New counter value: %d\n", tid, counter);
// 释放信号量
sem_post(&semaphore);
pthread_exit(NULL);
}
int main() {
pthread_t threads[NUM_THREADS];
// 初始化信号量
sem_init(&semaphore, 0, 1);
for (long i = 0; i < NUM_THREADS; i++) {
pthread_create(&threads[i], NULL, increment_counter, (void*)i);
}
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
// 销毁信号量
sem_destroy(&semaphore);
printf("Final counter value: %d\n", counter);
return 0;
}条件变量 cond
条件变量常用函数:
- pthread_cond_init:初始化条件变量。
- pthread_cond_wait:等待条件变量,将当前线程阻塞,直到其他线程调用 pthread_cond_signal 或 pthread_cond_broadcast 通知条件满足。
- pthread_cond_signal:唤醒至少一个等待的线程。
- pthread_cond_broadcast:唤醒所有等待的线程。
- pthread_cond_destroy:销毁条件变量。
条件变量使用的基本流程如下: 在需要进行同步的地方,使用互斥锁(pthread_mutex_t)保护共享数据的访问。 当条件不满足时,调用 pthread_cond_wait 等待条件变量。 当其他线程满足了条件,通过 pthread_cond_signal 或 pthread_cond_broadcast 唤醒等待的线程。 被唤醒的线程从 pthread_cond_wait 返回,重新获取互斥锁,并检查条件是否满足,直到满足条件后继续执行。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define BUFFER_SIZE 6
int buffer[BUFFER_SIZE];
int buffer_count = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t buffer_empty_condition = PTHREAD_COND_INITIALIZER;
pthread_cond_t buffer_full_condition = PTHREAD_COND_INITIALIZER;
void* producer(void* arg) {
for (int i = 0; i < BUFFER_SIZE * 2; i++) {
pthread_mutex_lock(&mutex);
// 等待缓冲区不满
while (buffer_count == BUFFER_SIZE) {
pthread_cond_wait(&buffer_empty_condition, &mutex);
}
// 生产数据
buffer[buffer_count] = i;
buffer_count++;
printf("Producer produced data: %d\n", i);
// 唤醒消费者线程
pthread_cond_signal(&buffer_full_condition);
pthread_mutex_unlock(&mutex);
}
pthread_exit(NULL);
}
void* consumer(void* arg) {
for (int i = 0; i < BUFFER_SIZE * 2; i++) {
pthread_mutex_lock(&mutex);
// 等待缓冲区不空
while (buffer_count == 0) {
pthread_cond_wait(&buffer_full_condition, &mutex);
}
// 消费数据
int data = buffer[buffer_count-1];
buffer_count--;
printf("Consumer consumed data: %d\n", data);
// 唤醒生产者线程
pthread_cond_signal(&buffer_empty_condition);
pthread_mutex_unlock(&mutex);
}
pthread_exit(NULL);
}
int main() {
pthread_t producer_thread, consumer_thread;
// 创建生产者和消费者线程
pthread_create(&producer_thread, NULL, producer, NULL);
pthread_create(&consumer_thread, NULL, consumer, NULL);
// 等待线程结束
pthread_join(producer_thread, NULL);
pthread_join(consumer_thread, NULL);
return 0;
}