C/C++ 知识点总结
语法相关
++j 和 j++
j = j++
和j = ++j
- 根据编译器的不同,c++中
j = j++
的执行结果也会不同。 - gnu gcc 5.4.0 汇编结果:
j = j++
先 ++ 再 =赋值
- 根据编译器的不同,c++中
输入输出
(1) 第一行输入两个整数M,N;接下来M行分别输入2个数,最后一行输入N个数(由于输入行数,个数都确定,用循环很好处理)
(2)循环输入多行
while(cin>>m>>n){
}
while((scanf("%d",&arr[i])!=EOF) && getchar() !='\n'){
}
(3)输入一行数字,数字间以空格分隔
istringstream类中构造字符串流时,空格会成为字符串参数的内部分界;
istringstream 可用于string与各种类型的转换途径
#include<sstream>
int main(){
string str;
while(getline(cin,str)){
vector<int> num;
int temp;
istringstream iss(str);
while(iss>>tmp){
num.push_back(tmp);
}
}
}
参考:C++ stringstream介绍,使用方法与例子
(4)stringstream的用法
string str1="hello, world!";
istringstream iss;
iss.str(str1); // 将string 类型的str1复制给iss, 返回void
string s;
while(iss>>s){
cout<<s<<endl; // 按空格读取string
}
(5)输出精度
#include<iomanip>
cout << fixed << setprecision(5) << f << endl;
参考资料:
C++性质/面向对象特点:封装、继承、多态
- 封装可以隐藏实现细节,使得代码模块化——目的是代码重用。
- 继承可以扩展已存在的代码模块——目的是代码重用。
- 多态:一方面是实现代码重用,另一方面使得子类重写父类的方法,提高程序的可扩展性。
指针和引用的区别
- 指针需要分配内存空间;引用只是变量的别名,不需要分配内存空间;
- 引用定义时必须初始化,且不能改变;(注意引用的值不能为NULL)
- 有多级指针,但没有多级引用;
- 指针和引用自增结果不一样;
- sizeof不一样
- 引用是一个变量的直接访问,指针是间接
重载、覆盖(重写)和隐藏
- Function overloading is a feature of Object Oriented programming languages like Java and C++. As we know, C is not an Object Oriented programming language. Therefore, C does not support function overloading. (C中 定义相同名字的函数会报错)
- 函数名相同参数列表相同返回值不同不构成重载(会被认定为重复定义,编译报错)
- 覆盖配合虚函数实现多态。正常情况下,基类指针指向子类对象时,是无法访问子类函数的(编译时报错),通过定义虚函数,使得基类指针指向子类对象时可以访问子类函数。(没有指定虚函数, 那么它就只能访问到类型对应的函数,基类指针就只能访问到基类函数,子类指针就只能访问到子类函数)
- 隐藏:满足隐藏条件时,子类对象无法调用基类函数,基类函数被隐藏
不同类型的数据进行运算
- unsigned int 和 int 相加,都先转换成unsigned int 类型,再相加
- int 和 short 或者 char 相加,都先转换为 int 类型,再相加
- char 和 char 相加,都先转换为int类型,再相加
- 下图的横向箭头表示一定会发生的转换,比如两个float类型相加,则都先转换成double类型再相加;纵向箭头表示两个运算数类型不一致时会发生的转换,比如unsigned int 和 int 相加,会把int 先转换为unsigned int 再相加
unsigned int a = 10;
int b = -19;
cout<< a+b <<endl; // 输出4294967287
参考:
不同类型的数据进行运算
C语言不同数据类型间的混合运算+常见数据类型
C语言中数据类型的自动类型转换
内存对齐
C++的异常安全性
如果异常被抛出,应满足:
- 不泄露任何资源
- 不允许破坏数据
C++中异常安全函数提供了三种安全等级:
1、基本承诺:如果异常被抛出,保证不泄露资源,不破坏数据,但对象前后状态可能发生变化。
2、强烈保证:如果异常被抛出,保证对象的状态保持不变。
3、不抛异常保证:保证不抛出任何异常。
常用函数
strcpy 与 memcpy
- memcpy用来做内存拷贝,可以拷贝任何数据类型对象,可以指定拷贝数据长度
- strcpy只能拷贝字符串,遇到’\0’结束
- memcpy(d,s,sizeof(d)); // s为源地址,d为目标地址,sizeof(d)为拷贝的字节数,头文件<string.h>
参考资料:
C函数之memcpy()函数用法
rand() ( < cstdlib >头文件)
- rand() 返回 0~RAND_MAX(0x7fff)间的随机数
- rand()%100 生成 [0, 99]间的随机数
- srand((int)time(0)); // 生成随机数种子 (time()的头文件为<ctime>)
#include<iostream>
#include<cstdlib>
#include<ctime>
using namespace std;
int main(){
srand((int)time(0));
for(int i=0;i<10;++i)
cout<<rand()%100<<endl; // 生成10个[0,99]间的随机数
}
setprecision (头文件 < iomanip >)
double a=123.456;
cout<<setprecision(5)<<a<<endl; // 输出5位有效数字 123.46
// 当有效数少于要显示数的位数时,采用舍入,而非截断数字
// 末尾0将被省略
cout<<setiosflags(ios::fixed)<<setprecision(2); // 保留两位小数,只需设置一次
cout<<fixed<<setprecision(2); // 同上
printf("面积=%.2f\n",PI); // 保留两位小数
C++关键词
namespace
-
作用:由于在同一个作用域内,一个名字只能表示一个实例,如果在同一个作用域定义了两个相同名字的变量,就会编译报错。
-
使用namespace,可以在不同的namespace内定义相同名字的变量,使用变量时注意需要用 namespace_name::val_name 引用变量。
-
类和函数可以在命名空间内创建,也可以在命名空间内声明,在命名空间外定义,定义时需要引用namespace_name,如下所示
namespace ns { // Only declaring class here class geek; } // Defining class outside class ns::geek { public: void display() { cout << "ns::geek::display()\n"; } }; // Creating a namespace namespace ns { void display(); class geek { public: void display(); }; } // Defining methods of namespace void ns::geek::display() { cout << "ns::geek::display()\n"; } void ns::display() { cout << "ns::display()\n"; }
-
匿名命名空间(unnamed namespace):仅在当前文件可访问,可作为声明静态变量的一种替代
-
命名空间是可嵌套的,引用变量时注意要按层级引用命名空间,例如 out::in::val
-
别名
namespace alias = name1::name2::name3
参考:
[1] namespace1
[2] namespace2
const int*, int const*, int *const, const int* const 的理解
- const 总是作用于前面的类型,如 int const*, 这里的const作用于int,表明指针所指向的数据是常量
- const int 和 int const 效果一样,都表示指针所指向的数据是常量
- int *const, const作用于*,表明指针为常量
参考:用最好的方法去理解const int*, int const*以及int *const
inline
- 内联函数,在函数调用处展开,省去了调用函数的开销,提高代码的执行效率
- 对于过长的代码,会导致展开失败。
extern "C"用法
- 目的:实现C++与C及其他语言的混合编程(注意C++可以编译C文件,但是用C++编译的文件没法调用用C编译的接口,extern “C” 代码一般加于C项目代码的头文件,方便C++代码调用)
- extern 表示本模块以及外部模块可以使用这部分函数及变量
- 指示编译器这部分代码按C语言进行编译链接
- 由于C++支持函数重载,在编译函数过程中会将函数参数类型也加入编译后的代码中;C语言不支持函数重载,因此编译C语言代码的函数不会带上函数的参数类型。(因此C++不能直接调用C接口,两种编译方式不同)
参考:
C++是如何调用C接口的?
C++中extern “C”含义深层探索
C++中的new/delete
- malloc分配时,如果内存分配失败,会返回NULL
- new分配内存失败时存在两种机制,例如gcc编译器会抛出异常,VC++6.0会返回空指针。我们可以使用 std::nothrow 让其不抛出异常并返回空指针。
int m, n;
cin>>m>>n;
int** p = new int*[m];
for(int i=0;i<m;++i){
p[i] = new int[n];
}
char *p1 = new (std::nothrow) char[100000000000];
if (p1 == 0) {
cout<<"fail"<<endl;
}
参考:
[1] std::nothrow
定位new(placement new)
- 在指定内存中创建对象
char buffer[1024]={0}; // 先分配一块内存区域
int* p= new (buffer) int[5]; // 在预分配的内存区域上创建对象。
new/delete 和 malloc/free的关系
- 它们都用于动态申请和释放内存。
- malloc/free 是C标准库函数,new/delete是C++运算符。
- new/delete会自动调用构造函数/析构函数,适用于非内部数据类型对象。
- new无需指定分配的内存大小,会根据类型自动计算;malloc需要指定分配的内存大小;
- 分配成功时,new返回对象类型指针,malloc返回void*,需要进行强制类型转换,因此new是类型安全的;
- 分配失败时,new会抛出异常,malloc返回NULL;
- new 会先申请内存空间(调用malloc),然后调用构造函数,初始化成员变量,再返回对象类型指针;delete先调用析构函数,再调用free释放内存;
- C++允许new/delete 运算符重载,不允许malloc重载;
- new从自由存储区分配内存,malloc从堆上分配内存。
delete 与 delete[] 的区别
- delete只调用一次析构函数,delete[]会调用每一个成员的析构函数(即当delete 一个数组时,需要为数组的每个元素调用析构函数,就需要用到delete[])
- 对于内建简单数据类型,delete与delete[]功能相同(因为内建数据类型无析构函数);对于自定义数据类型,不能互用。
INT_MAX和INT_MIN
volatile
- volatile用来解决共享环境下容易出现读取错误的问题。
- volatile 用于修饰变量,表明该变量是易变的,编译器对访问该变量的代码就不再进行优化,当要使用该变量时系统总会重新从它所在的内存中读取数据。
- 可以把一个非volatile int 赋值给 volatile int,但不能把非volatile对象赋给volatile对象。
- 当两个线程都用到某个变量且该变量的值会被改变时,应该声明为volatile, 意思是让编译器每次操作该变量时一定要从内存中取值,而不是使用寄存器中的值。
// 1、修饰由指针指向的对象,表明数据是const或volatile
const char* cp;
volatile char* vp;
// 2、修饰指针,表明指针是const或volatile
char* const cp;
char* volatile vp;
const, enum 和 #define
- 若用#define定义了宏,则其在后续的编译过程中有效(除非被#undef)
- define 不能用来定义class专属常量,也不能提供任何封装性
- const 可以用来声明类内常量,同时用static 确保该常量至多只有一份
- 一个属于枚举类型的值可以被充当int使用
- enum和#define不会导致非必要的内存分配
- 对于形似函数的宏,不会产生函数调用带来的额外开销,但是容易产生很多意想不到的状况,常用inline函数替换#define
// 注意某些编译器不支持static成员在声明式上获得初始值,此时可以在类外定义时初始化
class A{
private:
static const int Num=5; // 声明常量
int scores[Num]; // 使用该常量
};
const int A::Num; // 类外定义
class B{
private:
enum{Num=5};
int scores[Num];
};
#define CALL_WITH_MAX f((a)>(b)?(a):(b))
// 可以用inline替换
template<typename T>
inline void callWithMax(const T& a, const T& b){
f(a>b?a:b);
}
- 若想避免 if(a*b=c) 的错误,在重新定义 * 操作符函数时,可将返回值声明为const类型
强制类型转换
隐含类型转换
class A{
};
class B: public A{
};
A* pA;
B* pB;
pA = pB; // 正确
pB = pA; // 编译错误, C++ 不允许将基类指针隐含转换为派生类指针,不允许非公有继承的隐含转换。
static_cast
- 用于强迫隐式转换,如将非常对象转换为常对象,编译时检查,用于非多态的转换,可以转换指针,但 没有运行时类型安全检查来保证转换的安全性。
- 用法一:基类和派生类之间指针或引用的转换。基类必须是子类的公有基类,其他则会出现编译错误。
上行转换是安全的(将子类指针转换为基类指针),可以有虚函数,继承方式必须是公有的;
下行转换由于没有动态类型安全检查是不安全的(将基类指针转换为派生类指针),但编译可以通过。继承方式必须是公有的。 - 用法二:基本数据类型之间的转换,转换安全性也需要开发人员保证。
- 用法三:把空指针转换成目标类型的空指针。
- 用法四:把任何类型的表达式转换成void类型
- 注意:static_cast不能转换掉expression的const、volatile、或者__unaligned属性。( 不能把常对象转换为非常对象;)
dynamic_cast
- 主要用于类层次间的上行转换和下行转换以及类之间的交叉转换
- 进行下行转换时, 具有类型检查的功能,比static_cast 更安全,下行转换基类必须包含虚函数。
- 用法一:Base为至少包含一个虚函数的基类,Derived为Base的公有派生类,则可在运行时将基类的指针转换为派生类的指针。
因为dynamic_cast会进行运行时类型检查,需要运行时的类型信息,而这个信息存储在类的虚函数表中,只有定义了虚函数的类才有虚函数表,所以基类一定要包含虚函数。
注意:
- 其他三种都是编译时完成的,dynamic_cast运行时要进行类型检查。
- 不能用于内置基本数据类型的强制转换。
- 转换成功返回类的指针或引用,失败返回NULL
- 基类一定要有虚函数
- 要求<>内部所描述的目标类型必须为指针或引用
(为什么要进行运行时类型安全检查?我们知道static_cast也能将基类指针转换为派生类,这时用派生类指针去调用一个只存在于派生类的函数时,编译期不会产生问题,但是会导致运行时错误,因为该指针实际指向基类对象,而基类不包含该函数。)
const_cast
- 用于去除const或volatile属性
- 用法一:常量指针被转换为非常量指针,并且仍指向原来的对象。
- 用法二:常量引用被转换为非常量的引用,并且仍指向原来对象。
- 用法三:用于修改底指针,如const char* p
注意:只能用去去除指针或引用的常量性,不能用于变量。
reinterpret_cast
- 不可移植
- 在执行向上转化时,例如将一个派生类指针转换成基类指针,static_cast会计算父子类转换的偏移量,并将之转换到正确的地址;而reinterpret_cast不会。
- 用法一:指针->指针的转换(如int* 转换成double*),引用间的转换
- 用法二:指针/引用->整型
- 用法三:整型->指针/引用
参考:
C++强制类型转换:static_cast、dynamic_cast、const_cast、reinterpret_cast
C++ 四种强制类型转换
STL
vector
vector<vector<int> > p(m,vector<int>(n));
queue
#include<queue>
queue<int> queue1; // 创建队列
queue1.push(x); // 入栈,将x添加到队列的末尾
queue1.pop(); // 出栈,无返回值
queue1.front(); // 访问队首元素
queue1.back(); // 访问队尾元素
queue1.empty(); // 当队列为空,返回true
queue1.size();
priority_queue (头文件 < queue >)
- 默认使用 less , 按从大到小排序;greater 按从小到大排序;
//下面两种优先队列的定义是等价的
priority_queue<int> q;
priority_queue<int,vector<int>,less<int> >;//后面有一个空格
map
// map 默认按键值从小到大排序
map<int, int, less<int> > myMap;
参考:
c++ map自定义排序
C++容器迭代器失效
- vector迭代器失效的情况:
插入元素时,如果内存不够大,需要重新分配内存,这时候之前元素的迭代器会失效;
C++的类
静态成员变量和静态成员函数(static)
- 静态成员变量是属于整个类的,用于同一类不同对象间的数据共享,需要在类外进行初始化,可以通过类名或对象名调用。
- 静态成员变量也是属于整个类的,它没有this指针,只能访问静态成员变量。
常对象和常成员函数
- 是否为常成员函数构成函数重载
- 常对象只能调用常成员函数
- 常成员函数内部不能调用非常成员函数(编译错误)
- 声明为mutable的成员变量可在常成员函数中被修改值
拷贝构造函数和赋值函数
- 为什么拷贝构造函数要采用引用:因为基类的引用是可以绑定一个派生类的对象的,因此将基类的拷贝构造函数声明为引用,可以用一个派生类对象去初始化基类对象。拷贝构造函数的参数应声明为引用,若传值会导致无限调用拷贝构造函数。
- 为什么赋值函数要采用引用:同上,采用引用,可以将一个派生类对象赋值给基类对象。赋值函数的返回值应声明为引用,允许连续赋值str1=str2=str3。参数应声明为常引用,避免调用拷贝构造函数的开销。
- 原则上应为所有包含动态分配成员的类提供拷贝构造函数和赋值函数以及析构函数,在拷贝构造函数中,要申请新的内存空间并复制原有内容;在赋值函数中,要释放对象的原有内存,再申请新的内存,(不能简单的丢弃原有指针,会导致内存泄漏;另外也无法处理 A a; a=a; 的问题)
A a;
A b=a; // 定义对象时用已经存在的类对象去初始化,调用拷贝构造函数,与 A b(a) 相同
A c;
c = a; // 将已存在的对象a赋值给已存在的对象c,调用赋值函数
class A{
public:
A() { pBuffer=NULL; nSize=0; } // 构造函数
~A() { delete pBuffer; } // 析构函数
A( const A& obj) {
nSize = obj.nSize;
pBuffer = new char[nSize]; // 开辟新的内存空间
memcpy(pBuffer, obj.pBuffer, nSize*sizeof(char)); // void *memcpy(void*dest, const void *src, size_t n); 由src指向地址为起始地址的连续n个字节的数据复制到以destin指向地址为起始地址的空间内,返回一个指向目标内存空间的指针。头文件#include<string.h>
}
A& operator = (const A& obj){
// if(this==&obj) return *this; // 判断是否为同一对象。
nSize = obj.nSize;
char* tmp = new char[nSize];
memcpy(tmp, obj.pBuffer, nSize*sizeof(char));
delete[] pBuffer; // 删除原有内容
pBuffer=tmp; // 建立新指向
return *this;
}
// 另一种方法:先创建一个临时对象
A& operator = (const A& obj){
if(this!=&obj){
A tmpA(obj);
char* tmp=pBuffer;
pBuffer=tmpA.pBuffer;
tmpA.pBuffer=tmp;
}
return *this;
}
void Init( int n ){
pBuffer = new char[n];
nSize = n;
}
private:
char* pBuffer;
int size;
};
空类
- sizeof() 大小为1个字节,因为类的实例化需要分配内存空间,至少1个字节。
- 空了包含6个默认成员函数:构造函数,复制构造函数,赋值运算符函数,析构函数,取地址操作符函数,被const修饰的取地址操作符函数
继承
默认是私有继承。
多态
- C++的多态分为编译时多态和运行时多态。
- 编译时多态体现在函数和运算符重载上(编译时就确定调用函数的类型);运行时多态通过继承和虚函数来体现。
虚函数
- 虚函数的作用是实现多态。基类定义了虚函数,子类可以重写该函数,当子类重新定义了父类的虚函数后,父类的指针可以根据指向的对象的不同调用父类或子类的成员函数,这样的函数调用只能在运行期确定,也叫迟绑定。(函数重载在编译器确定,为早绑定)。
- 编译器会为每一个包含虚函数的类创建一个虚表,这个表是一个一维数组,存储着每个虚函数的地址。编译器还为每个类的对象提供一个虚表指针,指向对象所属类的虚表。在程序运行时,根据对象的类型去初始化虚表指针,从而找到正确的函数。
抽象类/纯虚函数
- 抽象类是包含纯虚函数的类。
- 只有声明,没有实现,不能实例化。
C++11特性
今天看到一个题解如下,来源在这。总结一下一些用法。
lambda表达式
- [捕获列表] (参数列表) mutable exception -> 返回类型 {函数体} (传参)
- [=]:按值捕获,函数体内不能修改其值(会引发编译错误),若要改,需要加mutable(由于传值,外部变量值依然不会被改变); [&]:按引用捕获i
- 返回类型可以不加,当只有一个return时,可以自动推断返回类型;
using
大多数时候using 被用来指定命名空间:using namespace std;
C++11中提出通过using指定别名,就是这里用到的:using Counter = unordered_map<char,int>, 为变量类型指定一个别名。相对于typedef更清晰易读,且using 可以为模板指定别名,这一点是typedef做不到的。详细参考C++ 中using 的使用。
auto
C++11中引入的auto主要有两种用途: 自动类型推断和返回值占位。参考【C++11】新特性——auto的使用。
可以使用*, &, const 来修饰auto;
auto k=5;
auto c = 'A';
auto s("hello");
auto* pK = new auto(k);
const auto n=6;
// 一些注意事项:
(1) 如果初始化表达式是引用,则去除引用语义
int a = 10;
int& b = a;
auto c = b; // c的类型为int, 去除引用
auto& d = b; // 此时c的类型为int&
c=100; // a=10
d=100; // a=100
(2) 如果初始化表达式是const 或者 volatile,则去除引用语义
const int a1 = 10;
auto b1 = a1; // b1 为 int 类型 而非 const int
const auto c1 = a1; // c1 为 const int
b1 = 100; // 合法
c1 = 100; // 非法
(3) 如果auto 加上&, 则不去除const 语义
const int a2 = 10;
auto& b2 = a2; // b2 类型为 const int
b2 = 10; // 非法
(4) 初始化表达式为数组时, 自动推断为指针
int a[3] = {1,2,3};
auto b3 = a; // b3 为int* 类型
(5) 若初始化表达式为数组且auto加上&,则推导类型为数组类型
int a[3] = {1,2,3};
auto& b = a; // b 为 int [3] 类型
(6) 函数或者模板参数不能声明为 auto
(7) auto只是一个占位符,不是一个真正的类型,不能使用sizeof
cout << sizeof(auto) << endl; // 错误
cout << typeid(auto).name() << endl; // 错误
(8) 使用auto声明的变量必须初始化;
auto a; // 错误
auto int a = 11; // 错误,C++11中已删除auto临时变量的语义
参考:https://www.cnblogs.com/QG-whz/p/4951177.html
for(auto& a:b)用法
c++11引入基于范围的for循环。使用条件:迭代范围确定;迭代的对象要实现++和==操作。参考内联函数、const、auto(C++11)、基于范围for循环(C++11)和nullptr(C++11。
这里用for(auto& a:b)的方式访问unordered_map<char,int>中的元素。
0829BIGO视频面
智能指针
shared_ptr
是一种智能指针。采用引用计数的机制,若对象多了一个智能指针指向它,则引用计数加1,若一个指向该对象的智能指针被销毁,引用计数减1。一旦某个对象的引用计数为0,则该对象会被自动删除。
shared_ptr原理是用px来记录指针,用 ∗ p n *pn ∗pn来引用计数,当一个shared_ptr对象达到作用域时,不会释放资源,只有当 ∗ p n *pn ∗pn变为0,才会释放指针指向资源。 进行拷贝构造时,将引用计数加1,并将pn 和 px的值复制过去。
shared_ptr类的构造函数加了explicit 关键字,因此不能进行隐含转换
比如有构造函数 A(int s): data(s){}; 有A* b(3); 则这样写是错误的 shared_ptr c=b; // 即不能将纯指针赋值给shared_ptr,不允许隐含转换
// 头文件
#include<memory>
// shared_ptr 三种初始化方法
(1)指向堆上申请的空间
int* a = new int(100);
std::shared_ptr<int> ptr(a);
(2)make_shared
std::shared_ptr<int> ptr1 = std::make_shared<int>(15);
(3)拷贝初始化
std::shared_ptr<int> ptr2(ptr1);
std::shared_ptr<int> ptr3=ptr1;
参考: C++之shared_ptr总结
// 注意事项
(1) 智能指针不能直接赋值(禁止纯指针给智能指针赋值或者拷贝构造)
// 不允许用一个纯指针给一个智能指针赋值或者copy构造,只能使用智能指针给另一个智能指针赋值或者copy构造。
// 这是因为shared_ptr的构造函数加了explicit 关键字,不允许隐含转换。且其拷贝构造函数以及赋值重载函数参数都是 const shared_ptr& 类型
std::shared_ptr<int> ptr2 = new int(5); // 错误
int* a = new int(2);
shared_ptr<int> b=a; // 错误
b = a; // 错误
(2) 不要用栈上的指针初始化shared_ptr , 因为栈中的对象离开作用域本身就会析构一次
(3) 不要用两个智能指针指向同一个内存空间
int* a=new int(2);
shared_ptr<int> sp(a);
shared_ptr<int> sp1(a); // 错误
// 当构造sp的时候,sp.px 指向 *a, *(sp.pn)=1(即引用计数为1)
// 同样在构造sp1的时候,sp1.px指向*a, *(sp1.pn)=1(引用计数也为1)
// 因此sp到达作用域的时候, *(sp.pn)为0,就会释放内存;sp1再达到作用域的时候会再释放一次内存