9.30 开眼界的c++
记录一些不知道的c++的特性
g++ 编译
g++ hello.cpp -o h
./h
枚举类
枚举类里面的变量都是数字
enum color { red, green=5, blue } c; c = blue;
red --> 0 green --> 5 blue --> 6
c --> 6
条件编译
- 控制代码的运行
condition = 1 ---> run
condition = 0 ---> disable
#if(condition)
code
#elif(condition)
code
#else
code
using 和 typedef
c++ 中尽量使用 using 来取别名
类型转换
静态转换、动态转换、常量转换、重新解释转换
- 静态转换即python, java 中通常理解的强制类型转换,
int i=10; float f = static_cast(i); // 静态将 int 类型转换为 float 类型
- 动态转换,可以理解为java 中的向下转型,如果是安全的(上行转换)可以转换,如果不安全的(下行转换)转换类型直接报错,同时不支持不相干类之间的转换。
class Base{}; // 定义一个 Base 类 class Derived : public Base{}; // Derived 类继承 Base 类 Base* ptr_base = new Derived; // 基类指针变量 ptr_base Derived* ptr_derived = dynamic_cast<Derived*>(ptr_base); // 将基类指针变量转换为子类指针
- 常量转换,即去掉变量的常量性,常量指针被转换为非常量指针并且仍然指向原来的对象;常量引用被转换为非常量引用并且仍然指向原来的对象。
const int i = 10; int& r = const_cast<int&>(i); // 常量转换,将const int转换为int
- 重新解释转换等同于C中的小括号转换
他可以将指针转换为整数也可以将整数转换为指针,至于能不能转,会不会有问题,不会查验,相当于强制类型转换
所以重新解释转换和静态类型转换的区别在于是否可以作用于不相干的类之间的转换
int i = 10; float f = reinterpret_cast<float&>(i); // 重新解释将int类型转换为float类型
extern关键字
区分声明和定义,相当于头文件
0. 原来 extern 是有点像头文件一样的东西
1. extern 关键字用来声明变量,声明了就可以用了,定义变量只能定义一次。
2. 要区分声明、定义、初始化:定义包含了声明,但是声明不包含定义。声明是不会为变量开辟内存空间的
例如函数一样,**可以在 main 函数下面定义**,但是得在头部声明,这样 main 函数才能找得到函数
假如直接在main函数定义前定义了,就不需要声明了,main函数可以直接找到
// 函数声明
int func();
int main()
{
// 函数调用
int i = func();
}
// 函数定义
int func()
{
return 0;
}
4. extern 对变量的声明和定义的区别也是和函数一致的:
int a = 0; //定义并声明了变量 a
extern int a; //只是声明了有一个变量 a 存在,具体 a 在哪定义的,需要编译器编译的时候去找。
5. 以下语法不被允许,只有 extern 声明位于函数外部时,才可以含有初始化式
int main()
{
extern int a = 20;
std::cout <<
...
}
#define 和常量 const 的区别
类型和安全检查不同
宏定义是字符替换,没有数据类型的区别,同时这种替换没有类型安全检查,可能产生边际效应等错误;
const常量是常量的声明,有类型区别,需要在编译阶段进行类型检查
编译器处理不同
宏定义是一个"编译时"概念,在预处理阶段展开,不能对宏定义进行调试,生命周期结束与编译时期;
const常量是一个"运行时"概念,在程序运行使用,类似于一个只读行数据
存储方式不同
宏定义是直接替换,不会分配内存,存储于程序的代码段中;
const常量需要进行内存分配,存储于程序的数据段中
定义域不同
void f1 ()
{
#define N 12
const int n 12;
}
void f2 ()
{
cout<<N <<endl; //正确,N已经定义过,不受定义域限制
cout<<n <<endl; //错误,n定义域只在f1函数中
}
定义后能否取消
宏定义可以通过#undef来使之前的宏定义失效
void f1()
{
#define N 12
const int n = 12;
**#undef** N //取消宏定义后,即使在f1函数中,N也无效了
#define N 21//取消后可以重新定义
}
const常量定义后将在定义域内永久有效,在定义时必须赋初始值,要不然是错误的,除非这个变量是用extern修饰的外部变量
const int A=10; //正确。
const int A; //错误,没有赋初始值。
extern const int A; //正确,使用extern的外部变量。
是否可以做函数参数
宏定义不能作为参数传递给函数
const常量可以在函数的参数列表中出现
C++ 中的类型限定符
- const:定义常量,表示该变量的值不能被修改
- volatile:变量的内存可见性,表示变量更新的同时,载入其它核中的副本也被同时更新,确保线程每次都重新从内存中读取isNext的值
- restrict:用来修饰指针的,表明该指针指向一段固定内存
- mutable:表示类中的成员变量可以在const成员函数中被修改
class Example {
public:
int get_value() const {
return value_; // const 关键字表示该成员函数不会修改对象中的数据成员
}
void set_value(int value) const {
value_ = value; // mutable 关键字允许在 const 成员函数中修改成员变量
}
private:
mutable int value_;
};
- static:定义静态变量,声明周期在整个程序
- register:表示建议编译器将该变量存储在寄存器中
- explicit:表明一个类的构造器只能显示调用
class Test1
{
public:
Test1(int n)
{
num=n;
}//普通构造函数
private:
int num;
};
class Test2
{
public:
explicit Test2(int n)
{
num=n;
}//explicit(显式)构造函数
private:
int num;
};
int main()
{
**Test1 t1=12;//隐式调用其构造函数,成功**
**Test2 t2=12;//编译错误,不能隐式调用其构造函数**
**Test2 t2(12);//显式调用成功**
return 0;
}
C++ 存储类
- auto:自动推断变量类型,类型python那种动态类型
auto f=3.14; //double
- register 存储类
register 存储类用于定义存储在寄存器中而不是 RAM 中的局部变量
这意味着变量的最大尺寸等于寄存器的大小(通常是一个词),且不能对它应用一元的 '&' 运算符(因为它没有内存位置)
- static 存储类
全局变量和静态局部变量效果差不多,函数结束时,静态局部变量不会消失,每次该函数调用 时,也不会为其重新分配空间。它始终驻留在全局数据区,直到程序运行结束
#include <iostream>
void fn();
int main()
{
fn();
fn();
fn();
}
void fn()
{
static int n=10;
std::cout<<n<<std::endl;
n++;
}
- extern 存储类:声明,使其可见,用于不同文件之间的变量和函数的传递
第一个文件:main.cpp
#include <iostream>
int count ;
extern void write_extern();
int main()
{
count = 5;
write_extern();
}
第二个文件:support.cpp
#include <iostream>
extern int count;
void write_extern(void)
{
std::cout << "Count is " << count << std::endl;
}
编译执行结果
$ g++ main.cpp support.cpp -o write
$ ./write
Count is 5
- thread_local 存储类
使用 thread_local 说明符声明的变量仅可在它在其上创建的线程上访问。 变量在创建线程时创建,并在销毁线程时销毁。 每个线程都有其自己的变量副本。
thread_local int x;
C++ Lambda 函数
-
基本使用
1. 简单类型 []{} // 定义简单的lambda表达式 auto basicLambda = [] { cout << "Hello, world!" << endl; }; // 调用 basicLambda(); // 输出:Hello, world! 2. 参数类型,自动推断返回类型 [](x,x){} // 指明返回类型 auto add = [](int a, int b) { return a + b; }; // 调用 int sum = add(2, 5); // 输出:7 3. 完整类型,包含capture,参数,返回值 // 指明返回类型 auto add = [](int a, int b) -> int { return a + b; }; // 调用 int sum = add(2, 5); // 输出:7 -
深刻探究特有的 capture 表达式
💡 `[capture](parameters) mutable ->return-type{statement}`[capture] :捕捉列表。捕捉列表能够捕捉上下文中的变量供 lambda 函数使用
• []:默认不捕获任何变量; • [=]:默认以值捕获所有变量; • [&]:默认以引用捕获所有变量; • [x]:仅以值捕获x,其它变量不捕获; • [&x]:仅以引用捕获x,其它变量不捕获; • [=, &x]:默认以值捕获所有变量,但是x是例外,通过引用捕获; • [&, x]:默认以引用捕获所有变量,但是x是例外,通过值捕获; • [this]:通过引用捕获当前对象(其实是复制指针),对象本身; • [*this]:通过传值方式捕获当前对象,对象拷贝,其实是传值; 1. []:默认不捕获任何变量; #include <iostream> using namespace std; int main() { int i = 1024; auto func = [] { **cout << i;** }; // **wrong,i 不可用** func(); } // 结果报错,因为未指定默认捕获模式 2. [=]:默认以值捕获所有变量; #include <iostream> using namespace std; int main() { int i = 1024; auto func = [=]{ // [=] 表明将外部的所有变量拷贝一份到该Lambda函数内部 cout << i; }; func(); } 3. [x]:仅以值捕获x,其它变量不捕获; #include <iostream> using namespace std; int main() { int i = 1024, j = 2048; cout << "outside i value:" << i << " addr:" << &i << endl; auto fun1 = [i]{ cout << "inside i value:" << i << " addr:" << &i << endl; // cout << j << endl; // j 未捕获 }; fun1(); } 4. [=, &x]:默认以值捕获所有变量,但是x是例外,通过引用捕获; #include <iostream> using namespace std; int main() { int i = 1024, j = 2048; cout << "i:" << &i << endl; cout << "j:" << &j << endl; auto fun1 = [=, &i]{ // 默认拷贝外部所有变量,但引用变量 i cout << "i:" << &i << endl; cout << "j:" << &j << endl; }; fun1(); } // 输出,可以发现 i 的地址是一样,而 j 为拷贝的 i:0x7ffff6f7ba28 j:0x7ffff6f7ba2c i:0x7ffff6f7ba28 j:0x7ffff6f7ba38 5. [this]:通过引用捕获当前对象(其实是复制指针); #include <iostream> using namespace std; class test { public: void hello() { cout << "test hello!n"; }; void lambda() { auto fun = [this]{ // 捕获了 this 指针 this->hello(); // 这里 this 调用的就是 class test 的对象了 }; fun(); } }; int main() { test t; t.lambda(); }
c++面向对象
- 作用域区分符(::):指明一个函数属于哪个类或一个数据属于哪个类。
**::** 叫作用域区分符,指明一个函数属于哪个类或一个数据属于哪个类。
**::** 可以不跟类名,表示全局数据或全局函数(即非成员函数)。
int month;//全局变量
int day;
int year;
void Set(int m,int d,int y)
{
::year=y; //给全局变量赋值,此处可省略
::day=d;
::month=m;
}
-
inline关键字:【感觉比较鸡肋,对短小的代码倒是可以,多的就会引起代码膨胀】
- 为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了 inline 修饰符,表示为内联函数。
- 定义在类中的成员函数默认都是内联的。类外,比如通过作用域符在类外定义的函数不是默认内联的。所以说,inline 是一种"用于实现的关键字"
- 内联其实就是避免频繁调用函数,直接将调用函数的地方替换成代码。因此会造成代码的膨胀。所以将方法定义在类中不太好,应该在类外定义!
-
private 类型:在类里面不写是什么类型,默认是 private 的。
// width 就是一个私有成员,这一点与java不太一样
class Box
{
double width;
public:
double length;
void setWidth( double wid );
double getWidth( void );
};
- public继承、protected继承、private继承
// 继承后,访问属性会降级
1.public 继承:基类 public protected private 派生类:public, protected, private
class A{
}
class B: public A{
}
2.protected 继承:基类 public protected private 派生类:protected, protected, private
3.private 继承:基类 public protected private 派生类:private, private, private
注意,基类的private成员,不能被派生类访问,protected 和 public 倒是可以
-
类的构造函数:用来初始化对象的。且可以有神奇的使用初始化列表来初始化字段,
注意初始化列表是按照声明的顺序初始化的,与初始化列表的顺序无关
class C{
private:
double X;
double Y;
double Z;
public:
C();
}
// 初始化列表
C::C(double a, double b, double c):X(a), Y(b), Z(c){
...
}
// 如果你决定使用初始化列表,总是按照它们声明的顺序罗列这些成员。这将有助于消除混淆。
class Student1 {
public:
int a;
int b;
Student1(int i):b(i),a(b){ } // 异常顺序:发现a的值为0 b的值为2
// 说明初始化仅仅对b有效果,对a没有起到初始化作用
// Student1(int i):a(i),b(a){ } // 正常顺序:发现a = b = 2 说明两个变量都是初始化了的
}
-
类的析构函数:
- 类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。
- 析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。
class Line
{
public:
void setLength( double len );
double getLength( void );
**Line(); // 这是构造函数声明
~Line(); // 这是析构函数声明**
private:
double length;
};
-
拷贝构造函数:
-
其实类似构造函数一样,当出现赋值时会调用拷贝构造函数
-
如果在类中没有定义拷贝构造函数,编译器会自行定义一个。如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数
-
对象作为函数参数,会调用拷贝构造函数。换句话说函数的形参是拷贝构造函数构造的。(形参的值不一定等于实参的值,如果形参是一个对象,那么该形参的值取决于对象的拷贝构造函数是如何实现的),因为函数是对象形参的话,去使用拷贝构造函数会带来时间消耗,那么有没有办法不调用拷贝构造函数呢?肯定有!如果要确保实参的值不会改变,又希望避免复制构造函数带来的开销,解决办法就是将形参声明为对象的 const 引用
-
拷贝构造函数与深拷贝和浅拷贝有关。
- 当出现类的等号赋值时,会调用拷贝函数。
- 当类的数据成员中有指针类型时,我们就必须定义一个特定的拷贝构造函数。因为如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象。所以,这时,必须采用深拷贝。
-
// 形式
classname (const classname &obj) {
// 构造函数的主体
}
---------------------------------------------
// 例子
class Line
{
public:
int getLength( void );
Line( int len ); // 简单的构造函数
**Line( const Line &obj); // 拷贝构造函数**
~Line(); // 析构函数
private:
**int *ptr; // 如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数**
};
// 成员函数定义,包括构造函数
Line::Line(int len)
{
cout << "调用构造函数" << endl;
// 为指针分配内存
ptr = new int;
*ptr = len;
}
Line::Line(const Line &obj)
{
cout << "调用拷贝构造函数并为指针 ptr 分配内存" << endl;
ptr = new int;
*ptr = *obj.ptr; // 拷贝值
}
Line::~Line(void)
{
cout << "释放内存" << endl;
delete ptr;
}
int Line::getLength( void )
{
return *ptr;
}
void display(Line obj)
{
cout << "line 大小 : " << obj.getLength() <<endl;
}
// 程序的主函数
int main( )
{
Line line1(10);
Line line2 = line1; // 这里也调用了拷贝构造函数
display(line1);
display(line2);
return 0;
}
/* 输出:
1. line1 构造的时候会调用构造函数
2. line1 赋值给 line2 的时候会调用拷贝构造函数
3. line1 赋值给 display 的形式参数的时候,会调用拷贝构造函数,
4. 接着执行 dispaly 的方法
5. 方法结束后,释放形参内存会执行析构函数
6. 重复 3 ---> 5
7. 析构 line2
8. 析构 line1
*/
调用构造函数
调用拷贝构造函数并为指针 ptr 分配内存
调用拷贝构造函数并为指针 ptr 分配内存
line 大小 : 10
释放内存
调用拷贝构造函数并为指针 ptr 分配内存
line 大小 : 10
释放内存
释放内存
释放内存
-
friend关键字:友元函数和友元类,【区分成员函数】
- 友元函数没有this关键字
- 友元函数的形参是类的对象
- 友元函数可以访问类的private和protected
#include <iostream>
using namespace std;
class Box
{
double width;
public:
friend void printWidth( Box box ); // 友元函数的声明
void setWidth( double wid );
};
// 成员函数定义
void Box::setWidth( double wid )
{
width = wid;
}
// 请注意:printWidth() 不是任何类的成员函数,可以直接调用
void printWidth( Box box )
{
/* 因为 printWidth() 是 Box 的友元,它可以直接访问该类的任何成员 */
cout << "Width of box : " << box.width <<endl;
}
// 程序的主函数
int main( )
{
Box box;
// 使用成员函数设置宽度
box.setWidth(10.0);
// 使用友元函数输出宽度
printWidth( box );
return 0;
}
-
static静态变量:不能在类中初始化静态变量,只能通过作用域控制符(::)来初始化静态变量。
-
多继承
- 菱形继承:解决办法是,① 作用域控制符来控制(::);② 虚继承:在A和B中不再保存Base中的内容,保存了一份偏移地址,然后将Base的数据保存在一个公共位置处
class A{
}
class B{
}
class C: public A, public B{
}
// 虚继承解决环状(菱形)继承问题
// A->D, B->D, C->(A,B),这里D被C继承了两次,可以用虚继承解决
class D{......};
class B: public D{......};
class A: public D{......};
class C: public B, public A{.....};
// 修改为:
class D{......};
class B: virtual public D{......};
class A: virtual public D{......};
class C: public B, public A{.....};
// 虚继承--(在创建对象的时候会创建一个虚表)在创建父类对象的时候
- 运算符重载 operator 关键字:可以重定义或重载大部分 C++ 内置的运算符
class Box{
public:
// 重载 + 运算符,用于把两个 Box 对象相加
**Box operator+(const Box& b){**
}
}
int main(){
Box Box1;
Box Box2;
Box Box3;
// 把两个对象相加,得到 Box3
**Box3 = Box1 + Box2;**
}
- 虚函数
虚函数是为了解决多态性
虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。
我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。
比如
class Shape{
public:
int area(){}
}
class A: public Shape{
public:
int area(){}
}
class B: public Shape{
public:
int area(){}
}
int main(){
Shape *shape;
A a;
b b;
shape = &a;
shape -> area();//由于静态链接,也叫早绑定,所以调用的是 shape 的area();
shape = &b;
sgaoe -> area();//由于是静态链接,也叫早绑定,所以调用的是 shape 的area();
}
// 可以通过 virtual 关键字解决
class Shape{
public:
virtual int area(){}// 此时就可以一样的方法调用,执行不同逻辑,形成多态
}
// 纯虚函数,适用于父类仅给一个模板,抽象类
class Shape{
public:
// pure virtual function
virtual int area() = 0;
}
每个含有虚函数的类都有各自的一张虚函数表VTABLE
每个派生类的VTABLE继承了它各个基类的VTABLE
理解虚函数和虚表指针在类中的传递关系:
对象的虚表指针用来**指向自己所属类的虚表**,虚表中的指针会指向其继承的**最近的一个类的虚函数**
由于这三个类都有虚函数,故编译器为每个类都创建了一个虚表,
即类A的虚表(A vtbl),类B的虚表(B vtbl),类C的虚表(C vtbl)。
类A,类B,类C的对象都拥有一个虚表指针,*__vptr,用来指向自己所属类的虚表。
类A包括两个虚函数,故A vtbl包含两个指针,分别指向A::vfunc1()和A::vfunc2()。
类B继承于类A,故类B可以调用类A的函数,但由于类B重写了B::vfunc1()函数,
故B vtbl的两个指针分别指向B::vfunc1()和A::vfunc2()。
类C继承于类B,故类C可以调用类B的函数,
但由于类C重写了C::vfunc2()函数,
故C vtbl的两个指针分别指向B::vfunc1()(指向继承的最近的一个类的函数)和C::vfunc2()。
-
return *this 和 return this:
- return *this返回的是当前对象的克隆或者本身(若返回声明类型为A, 则是克隆, 若返回声明类型为A&, 则是本身 )
- return this返回当前对象的地址
-
多态:
- 静态多态:也称为编译期间的多态,编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用那个函数,如果有对应的函数就调用该函数,否则出现编译错误
- 动态多态:即运行时的多态,在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。
- 虚函数 vitural 关键字
- 纯虚函数:virtual void funtion1()=0; 包含纯虚函数的类是抽象类,抽象类不能定义实例。
- 虚函数表:每个含有虚函数的类都有各自的一张虚函数表VTABLE。VTABLE表可以看成一个函数指针的数组,每个虚函数的入口地址就是这个数组的一个元素。
- 虚函数可以为private, 并且可以被子类覆盖。因为重写之时并没有与父类发生任何的调用关系,故而重写是被允许的。
- 类的内存长度:① 当基类中没有定义虚函数时,其长度=数据成员长度;派生类长度=自身数据成员长度+基类继承的数据成员长度;② 当基类中定义虚函数后,其长度=数据成员长度+虚函数表的地址长度;派生类长度=自身数据成员长度+基类继承的数据成员长度+虚函数表的地址长度。
-
c++接口和抽象类是通过纯虚函数实现的
c++ 高级
-
c++ 文件和流
- ofstream : 创建文件,并向文件写入
- ifstream:从文件读取
- open :例如打开并初始化文件,outfile.open("file.dat", ios::out | ios::trunc );
- 判断文件以EOF结尾,这里有个bug地方,不太理解
- cin.ignore():刷新缓冲区
-
c++ 堆区动态分配内存
- new: new 内置类型(比如:new double); new 数组(比如:new int [10])
- delete: delete 内置类型(比如:delete value);delete 数组(比如:delete [] value)
- 二维数组:
int **p; p = new int *[4]; - 三维数组:
int ***array; array = new int **[m];
-
命名空间
- namespace + using 关键字:不同命名空间下,有各自的函数,即使函数名相同,也不会冲突了
#include <iostream> using namespace std; // 第一个命名空间 namespace first_space{ void func(){ cout << "Inside first_space" << endl; } } // 第二个命名空间 namespace second_space{ void func(){ cout << "Inside second_space" << endl; } } int main () { // 调用第一个命名空间中的函数 first_space::func(); // 调用第二个命名空间中的函数 second_space::func(); return 0; }- 嵌套命名空间中的变量,全局变量,局部变量
c++ 模板
-
函数模板:template <typename T> void function(T &t, Y &y){...}
- 函数模板具体化:template<> void tfunc(const Node& node){...},就是让这个函数单独处理一个类型。函数模板的具体化和普通函数可以同时存在,调用顺序是 普通函数 > 函数模板具体化 > 模板函数,template<> void fun(int>(const int& n){...}
- 函数模板实例化:写好一个函数模板,然后写一个类似声明的东西加上具体参数类型就行。
-
类模板:类模板可以指定默认模板参数,函数模板不可以,template <typename T>
- 类模板的继承:① 继承的时候,子类为父类模板符号指定固定类型;② 子类自己有模板符号,和父类符号对应
- 类模板的多态:① 子类没有模板;② 子类有模板
- 类模板的具体化:就是一个参数指定模板符号,一个参数不指定
-
成员模板:定义在类内部的成员函数也可以是模板
预处理器
-
参数宏:
#define MIN(a, b) (a< b? a: b),可以当成函数调用 -
条件编译:
#ifdef variable ... #endif如果前面有定义define variable那么会执行code,如果没有那么就当初注释 -
和 ## 运算符
#define Do(x) #x#会把 x 用引号引起来,当成字符串处理
将参数连接
-
预定义宏变量:*\LINE\(打印当前代码行数);*\FILE\(当前文件名);*\DATE\(当前年月日);*\TIME\(当前时间)
c++ 信号处理
- 信号是中断处理,会提前终止一个程序。有一些信号宏变量
- signal(signal, handler)函数,signal就是宏变量,而handler是对应的回调处理函数,当出现信号的时候将它捕获
- raise(signal),用代码抛出一个信号
c++ 多线程
进程和线程
c++ cgi
略
c++ STL
STL 标准库包含了,
- 数据结构:vector、list、queue、dequeue、priority queue、stack、set、multiset、map、multimap、heap
dequeue: A queue data structure allows insertion only at the end and deletion from the front. 就像人们正儿八经的排队一样
- 算法:sort、binary Search