最近在复习之前学过的c++课程,整理下c++中一些零碎的知识点。
1. setprecision()为了精确小数位数,setiosflags(ios::fixed)为了保证当只有一位小数位且该小数位是0时,避免用e的指数输出。
cpp
#include <iostream>
#include <iomanip>
using namespace std;
int main(){
double cost=10.0;
cout << setiosflags(ios::fixed) << setprecision(1) << cost << endl;
}
# 10.0 如果不设置setiosflags,输出是1e+01
2. c++中表示次方
cpp
#include<cmath>
pow(height, 2) # height^2
3. switch...case语句,可以指定连续的case(3 4 5),也可以指定2个场景case(6 7)
cpp
switch(month){
case 3 ... 5: cout << "春季" << endl; break;
case 6:case 7: cout << "夏季" << endl; break;
...
}
4. cin>>s1这种方式输入,会自动去除开头的空白符号(空格、换行、制表符等),从第一个真正的字符开始读取,遇到下一个空白时结束。getline(cin, s1)读取一整行,遇到换行符结束。
cpp
#include <iostream>
#include <string>
using namespace std;
int main() {
string s1, s2;
// getline(cin, s1);
// getline(cin, s2);
cin >> s1 ;
cin >> s2;
cout << s1+s2 << endl;
return 0;
}
# 如果输入是" hello to\n world",那当前输出只会是"helloto";用getline才能保证输出是"
hello to world"
如果是用char数组定义的字符串,则是以下用法:
cpp
char str[100] = { 0 };
cin.getline(str, sizeof(str)); # 写入字符数组
5. char定义的字符数组,数组名指向第一个字符,但是cout打印数组名,编译器会自动输出后面的字符,在遇到'\0'后停止。字符数组名不能直接赋值,str2=str1是错误的,得用strcpy(str2, str1)。
cpp
char str[20] = "hello world";
cout << str;
char *p = NULL;
p = &str[6];
cout << p; # world
6. c++中数组名不能直接赋值,如arr2=arr1这样,数组名代表数组首地址,可以cout打印,输出数组首地址(str[]例外),数组想要打印出[1,2,3]这样的样式,只能自己去拼接[] 和逗号;
但是结构体可以struct2=struct1,但却不能cout打印;类对象也可以obj1=obj2,但不能cout打印。
7. 统计字符串长度:
如果是char a[] = xxx的定义方式,通过strlen(str1)获取,需要include<cstring>;
如果是string b=xxx的定义方式,通过b.size()或length(b)获取,需要include<string>;
以上统计都不包括末尾的'\0'字符,而sizeof()计算的是所占字节数,是包括'\0'字符的。
cpp
char str1[] = "hello world";
sizeof(str1) # 12
strlen(str1) # 11
char str2[20] = "hello world";
sizeof(str2) # 20
strlen(str2) # 11
8. 统计字符子串出现的次数
cpp
#include <iostream>
#include <cstring>
using namespace std;
int main() {
char str[100] = { 0 };
char substr[100] = { 0 };
cin.getline(str, sizeof(str));
cin.getline(substr, sizeof(substr));
int count = 0;
//转化为字符串
string str1(str);
string str2(substr);
int i=0;
//从str1下标i开始查找str2
while(str1.find(str2,i)!=-1){
//如果找得到,计数加1
count++;
//i从找到的位置,后移一位
i=str1.find(str2,i)+1;
}
cout << count << endl;
return 0;
}
9. 字符判断函数
字符判断函数 | 作用 |
---|---|
isalpha() | 判断字符是否是字母('a'-'z' 'A'-'Z') |
isdigit() | 判断字符是否是数字 |
isspace() | 判断字符是否是空格、制表符、换行等标准空白 |
isalnum() | 判断字符是否是字母或者数字 |
ispunct() | 判断字符是标点符号 |
islower() | 判断字符是否是小写字母('a'-'z') |
isupper() | 判断字符是否是大写字母('A'-'Z') |
cpp
string str1="hell123";
isdigit(str1[3]);
10. 函数中不能返回局部变量的引用或局部变量的地址。
cpp
#include <iostream>
using namespace std;
int * func(){
int a = 10; // 局部变量存放在栈区,栈区内存由编译器自动释放,函数执行完就会释放
int * b = &a;
return b;
}
//这里返回局部变量地址会报错
int main(){
int a = 5;
int *par = func();
cout << *par << endl; //第一次能打印成功,是因为编译器做了保留,其实这块内存已经被释放了
cout << *par << endl; //第二次打印是乱码
}
cpp
#include<iostream>
using namespace std;
//1、不要返回局部变量的引用
string& test(){
string a = "hello"; //局部变量存放在栈区,如果返回局部变量的引用,则函数定义就会报错
return a;
}
如果将上述代码的string &改为string, 则不会有问题,因为这样返回的其实是a的备份数据,而不是局部变量本身。
11. 深拷贝的使用
如果定义的类中,有属性需要在堆区开辟内存,则一定要提供拷贝构造函数,重新申请堆区内存保存属性,还有如果使用涉及=赋值(obj1=obj2),则一定要重载=操作符,否则都会报错重复释放。
cpp
#include<iostream>
using namespace std;
class Person{
public:
Person(int age){
this->age = new int(age);
}
Person(const Person & p){
this->age = new int(*p.age);
}
Person& operator=(Person &p){
//应该先判断是否有属性在堆区,如果有,先释放干净,再重新申请堆区内存
if(this->age != nullptr){
delete this->age;
this->age = nullptr;
}
this->age = new int(*p.age);
// return p; 用下面的*this也可以
return *this;
}
~Person(){
if(this->age != nullptr){
delete this->age;
this->age = nullptr;
}
}
int *age;
};
int main(){
Person p1(10);
Person p2(20);
Person p3(30);
p3 = p2 = p1;
cout << "p1.age = " << *p1.age << endl; # 10
cout << "p2.age = " << *p2.age << endl; # 10
cout << "p3.age = " << *p3.age << endl; # 10
}
12. 虚继承、(纯)虚函数、(纯)虚析构
虚继承一般用于处理多继承的关系,尤其是菱形继承时,孙类的属性不确定继承哪个子类,通过虚继承可以使孙类属性只保留一份数据。虚继承的原理和vbptr有关(virtual base pointer 虚基类指针,指向vbtable(虚基类表)),感兴趣的自行百度~
cpp
#include<iostream>
using namespace std;
class Animal{
public:
int age;
};
class Sheep: virtual public Animal{ //继承前加virtual关键字,代表虚继承;此时公共的父类Animal类称为虚基类
};
class Tuo: virtual public Animal{
};
class SheepTuo: public Sheep, public Tuo{
};
int main(){
SheepTuo st;
st.Sheep::age = 120;
st.Tuo::age = 25; //虚继承将变量只存一份
cout << st.Sheep::age << endl; # 25
cout << st.Tuo::age << endl; # 25
cout << st.age << endl; # 25
}
虚函数和虚析构都是多态中的概念,而多态使用要求父类指针或引用指向子类对象,然后用该指针或引用调用函数,如果父类中成员函数不定义为虚函数,则地址早绑定,编译阶段确定函数地址,运行时就只会调用父类的方法,而不是子类的方式。如果想要动态调用子类方式,则需定义为虚函数,在运行阶段绑定函数地址,哪个子类调用就调用对应子类的方法。
cpp
#include<iostream>
using namespace std;
class Animal{
public:
//不加入virtual,地址早绑定,编译阶段确定函数地址。调用func时就一直是Animal的地址
//加入virtual,在运行阶段绑定函数地址,谁调用就执行谁。
virtual void func(){ //称为 虚函数
cout << "动物方法" << endl;
}
};
class Cat: public Animal{
public:
void func(){
cout << "小猫方法" << endl;
}
};
class Dog: public Animal{
public:
void func() override{
cout << "小狗方法" << endl;
}
};
void dofunc(Animal *a){
a->func();
}
int main(){
Cat cat;
dofunc(&cat); # 小猫方法
cout << sizeof(Animal) << endl; //加入vitual,长度是8; 不加vitual,长度是1。本质是个虚函数(表)指针
}
使用多态时,父类中的成员函数一般无实际意义,可以改为virtual void func() = 0,这种就是纯虚函数,有纯虚函数的类称为抽象类。
再看一段代码:
cpp
#include<iostream>
using namespace std;
class Calculator{
public:
int a, b;
virtual int getResult() = 0;
Calculator(){
cout << "父类的构造函数" << endl;
}
~Calculator(){
cout << "父类的析构函数" << endl;
}
};
class AddCalculator: public Calculator{
public:
int * age;
int getResult(){
return a + b;
}
AddCalculator(int age){
this->age = new int(age);
cout << "子类的构造函数" << endl;
}
~AddCalculator(){
cout << "子类的析构函数" << endl;
if(age!=nullptr){
delete age;
age = nullptr;
}
}
};
int main(){
Calculator *c ;
c = new AddCalculator(90);
delete c;
}
父类的构造函数
子类的构造函数
父类的析构函数
如上代码所示,是没有调用子类的析构函数,那age内存空间就不能被正确释放。如果想释放age内存空间,则父类的析构函数前一定要加上virtual关键字,就是虚析构函数。
当子类中有属性开辟到堆区时,需要使用虚析构,否则析构父类时,不会调用子类析构函数,造成内存泄漏。如果写为virtual ~Calculator() = 0,则称为纯虚析构,与纯虚函数不同的是纯虚析构,也要有代码实现,如下所示:
cpp
class Calculator{
public:
int a, b;
virtual int getResult() = 0;
Calculator(){
cout << "父类的构造函数" << endl;
}
virtual ~Calculator() = 0; //纯虚析构,需要具体实现。
};
Calculator:: ~Calculator(){
cout << "父类的析构函数" << endl;
}
有纯虚函数或纯虚析构的类称为抽象类,注意抽象类都不能实例化对象。
13. vector容器
STL标准模板库,包括容器、算法、迭代器、仿函数等,其中vector是比较常用的容器。
vector 动态数组,单端数组。
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int var;
vector<int> a;
# 插入容器数据
while(cin>var){
a.push_back(var)
}
# 读取容器数据
for(vector<int>::iterator iv=a.begin();iv!=a.end();iv++){
cout << *iv << " ";
}
# 或者直接使用算法 print方法指定格式输出
for_each(a.begin();a.end();print);
# 可以直接赋值
vector<int> v2;
v2 = a;
# 删除最后一个数据
v2.pop_back();
# 在头部位置插入数据,插入2个100
v2.insert(v2.begin(), 2, 100);
# 删除数据
v2.erase(--v2.end()); # 删除最后一个数据
v2.erase(v2.begin(), ++v2.begin()); # 删除开头2个数据,传入的是一个指针区间
# 清除数据
v2.clear();
# 访问数据
a[7] a.front() a.back() # 下标访问、第一个元素、最后一个元素
# 容量和大小
a.capacity() # 一般比元素个数要大,指的是开辟的容量大小
a.size() # 元素个数
a.resize(3) # 重新指定元素个数,少了就会删除,多了就按默认值填充,int类型填充0;但是这种方式capacity不会改变
# 如果想要capacity和size一样大,那需要收缩空间,可以通过swap函数操作
vector<int>(a).swap(a)
# swap本身是交换函数,用于交换两个vector数组的内容,但地址不会交换
v1.swap(v2);
# reserve(int len) 容器预留len个元素长度,预留位置不初始化,元素不可访问。主要用于减少vector在动态扩展容量时的扩展次数。
vector<int> v1;
v1.reserve(100);
14. 标准算法
以下都是标准算法,注意不支持随机访问迭代器的容器,不能用标准算法,如list容器。
vector和deque都可以使用标准算法。
cpp
#include<algorithm>
sort(v.begin(), v.end()) # 指定区间 默认从小到大排序
reverse(v.begin(), v.end()) # 指定区间 倒序
replace(v.begin()++, --v.end(), 20, 200) # 指定区间内将20替换为200
vector<int> iterator::it = find(v.begin(), v.end(), 10)
if(it != v.end()){cout << "找到了";} # 从vector中查找元素10
int num = count(v.begin(), v.end(), 10) # 计算区间内元素10的个数有几个
15. 随机数实现
cpp
#include <iostream>
using namespace std;
#include <ctime>
int main(){
//添加随机数种子,作用:利用当前系统时间生成随机数,防止每次随机数都一样
srand((unsigned int)time(NULL)); //固定写法,记住就行。
//生成随机数
int num = rand()%100 + 1; // 0~99,加1后是1~100;