C++汇总一

1、static关键字的作用

1、全局静态变量:

  • 静态存储区,在整个程序运行期间一直存在。自动初始化为0。
  • 作用域:文件之外是不可见的。

2、 局部静态变量

  • 静态存储区,自动初始化为0。

  • 作用域:在函数或者语句块结束的时候,作用域结束,但没有销毁,只是不能访问。再次被调用时,值不变;

3、静态函数

  • 函数的定义和声明在默认情况下都是extern的
  • 静态函数只是在声明他的文件当中可见。只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突;
  • warning:
    • 头文件中声明的全局函数不要static,在cpp内声明的全局函数要static。
    • 要在多个cpp中复用该函数,就把它的声明提到头文件里去。

4、类的静态成员

  • 多个对象之间的数据共享。不会破坏隐藏的原则,保证了安全性
  • 对多个对象来说,静态数据成员只存储一处,供所有对象共用

5、类的静态函数

  • 共有,对静态成员的引用不需要用对象名。
  • 静态成员函数 中不能直接引用类的 非静态成员,可以引用类的 静态成员。
  • 静态成员函数 中要引用 非静态成员 时,可通过 对象 来引用。

2、C++和C的区别

设计思想:C++是面向对象的语言,而C是面向过程的结构化编程语言

语法上:

C++具有封装、继承和多态三种特性

C++相比C,增加多许多类型安全的功能,比如强制类型转换、

C++支持范式编程,比如模板类、函数模板等

extern “C”

  1. 指示编译器这部分代码按C语言(而不是C++)的方式进行编译。
  2. C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;
  3. 不可以将extern “C” 添加在函数内部
  4. 如果函数有多个声明,可以都加extern “C”, 也可以只出现在第一次声明中,后面的声明会接受第一个链接指示符的规则。
  5. 除extern “C”, 还有extern “FORTRAN” 等。
使用:
单一语句:
	extern "C" double sqrt(double);
复合语句
    extern "C"
    {
          double sqrt(double);
          int min(int, int);
    }
包含头文件,相当于头文件中的声明都加了extern "C"
	extern "C"
    {
        #include <cmath>
    } 

  • extern "C"修饰的变量和函数是按照C语言方式进行编译和链接的
  • 由于C++支持函数重载,而C语言不支持,因此函数被C++编译后在符号库中的名字是与C语言不同的;
  • C++编译后的函数需要加上参数的类型才能唯一标定重载后的函数
  • 如:int foo(int x, int y); 在C++中会被编译成 _foo_int_int。C 中会被编译成 _foo。
  • 在链接阶段,链接器会从模块A生成的目标文件moduleA.obj中找 _foo_int_int 这样的符号,显然这是不可能找到的,因为foo()函数被编译成了_foo的符号,因此会出现链接错误。

extern关键字

  1. extern是C/C++语言中表明函数和全局变量的作用范围的关键字,该关键字告诉编译器,其申明的函数和变量可以在本模块或其他模块中使用。

  2. extern int a; 仅仅是一个变量的声明,其并不是在定义变量a,也并未为a分配空间。变量a在所有模块中作为一种全局变量只能被定义一次,否则会出错。

  3. 通常在模块的头文件中 加入 给其他模块 引用的函数和全局变量以关键字extern生命。如果模块B要引用模块A中定义的全局变量和函数时只需包含模块A的头文件即可。

  4. extern对应的关键字是static,static表明变量或者函数只能在本模块中使用,因此,被static修饰的变量或者函数不可能被extern C修饰。

3、c++中四种cast转换

C语言的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。

四种类型转换是:static_cast, dynamic_cast, const_cast, reinterpret_cast。

  • const_cast:将const变量转为非const
  • static_cast:隐式转换。比如非const转const,void*转指针等。多用于多态向上转化,如果向下转能成功但不安全。
  • dynamic_cast:只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。
    • 向上转换:指的是子类向基类的转换
    • 向下转换:指的是基类向子类的转换
  • reinterpret_cast:几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;

4、C/C++ 中指针和引用的区别?

  1. 指针有自己的一块空间,而引用只是一个别名;

  2. 使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;

  3. 指针可以被初始化为NULL(未知的值),而引用必须被初始化且必须是一个已有对象 的引用;

  4. 作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;

  5. 可以有const指针,但是没有const引用;

  6. 指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变;

  7. 指针可以有多级指针(**p),而引用至于一级;

  8. 指针和引用使用++运算符的意义不一样;

  9. 如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。

检查内存泄漏神器valgrind https://zhuanlan.zhihu.com/p/75416381

5、私有构造函数的作用

class OnlyHeapClass  
{  
public:  
   static OnlyHeapClass* GetInstance(){  
       // 创建一个OnlyHeapClass对象并返回其指针  
       return (new OnlyHeapClass);  
   }  
   void Destroy(){
   		delete this;
   }  
private:  
       OnlyHeapClass() {}  
       ~OnlyHeapClass() {}  
};  
int main()  
{  
       OnlyHeapClass *p = OnlyHeapClass::GetInstance();  
       ... // 使用*p  
       p->Destroy(); // 不能直接 delete p;  
       return 0;  
}
  • class外部不允许访问私有成员,在程序中声明一个对象时,编译器无法调用构造函数。程序出错。
  • 利用static公有成员独立于class对象之外的特点,不必产生对象也可以使用它们。
  • 在static函数内部创建了该class的对象,并以引用或者指针的形式将其返回。
  • 由于要跨函数传递并且不能使用值传递方式,为了保证getInstance()退出后对象也不会随之释放,可以手动释放,我们需要在堆上创建对象。
  • 构造函数私有化的类的设计保证了其他类不能从这个类派生或者创建类的实例。
  • 构造函数私有化的类的设计可以保证只能用new命令在堆中来生成对象,只能动态的去创建对象,这样可以自由的控制对象的生命周期。但是,这样的类需要提供创建和撤销的公共接口。
  • 保证类在内存中至多存在一个,或者指定数量个的对象(可以在class的私有域中添加一个static类型的计数器,它的初值置为0,然后在GetInstance()作些限制:每次调用它时先检查计数器的值是否已经达到对象个数的上限值,如果是则产生错误,否则才new出新的对象,同时将计数器的值增1。)
  • 将构造函数设计成Protected,也可以实现同样的目的,但是可以被继承。

Q:为什么要自己调用析构函数呢?对象结束生存期时不就自动调用析构函数了吗?什么情况下需要自己调用析构函数呢?

A:比如在析构之前必须做一些事情,但是用你类的人并不知道,那么你就可以重新写一个函数,里面把要做的事情全部做完了再调用析构函数。这样人家只能调用你这个函数析构对象,从而保证了析构前一定会做你要求的动作。

Q:什么情况下才要生成堆对象呢?

A:堆对象是new出来的,栈对象不是。何时该用动态,何时该用静态生成的问题。这个要根据具体情况。

  • 比如你事先知道某个对象最多只可能10个,那么你就可以定义这个对象的一个数组。10个元素,每个元素都是一个栈对象。
  • 如果无法确定数字,那么你就可以定义一个这个对象的指针,需要创建的时候就new出来,并且用list或者vector管理起来。

Q:怎么生成堆对象?

A:C++是一个静态绑定语言,在编译过程中,所有的非虚函数调用都必须分析完成,即使是虚函数,也需要检查可访问性。当对象建立在栈上面时,是由编译器分配内存空间的,调用构造函数来构造对象。当对象使用完后,编译器会调用析构函数来释放栈对象所占的空间。

编译器管理了对象的整个生命周期。如果编译器无法调用类的析构函数,即析构函数是私有的,编译器无法调用析构函数来释放内存。所以上述例子类中必须提供一个Destroy函数,来进行内存空间的释放。类对象使用完成后,必须调用Destroy函数。

delete操作会调用析构函数,所以不能编译通过。提供一个Destroy成员函数,完成delete操作。在成员函数中,析构函数是可以访问的,当然detele操作也是可以编译通过。

Q:无法解决继承问题。

A:如果OnlyHeapClass作为其他类的基类,则析构函数通常要设为virtual,然后在子类重写,以实现多态。因此析构函数不能视为private。将析构函数设为protected可以有效解决这个问题,类外无法访问protected成员,子类则可以访问。为了统一,可以将构造函数也设为protected,然后提供一个public的static函数来完成构造,这样不用直接使用new而是使用一个函数来构造,使用一个函数来析构。

Q:构造和析构函数定义为私有的,阻止了用户在类域外对析构函数的使用。

A:具体表现为:

  • 禁止用户对此类型的变量进行定义,即禁止在栈内存空间内创建此类型的对象。要创建对象,只能用new在堆上进行。
  • 禁止用户在程序中使用delete删除此类型对象。对象的删除只能在类内实现,也就是说只有类的实现者才有可能实现类对象的delete,用户不能随便删除对象。如果用户想删除对象的话,只能按照类的实现者提供的方法进行。

6、常量指针和指针常量:

指针常量:	int * const p; 
	修饰p,指针p不能改,不能指向其他地址。指针指向的值*p可改。
常量指针:	const int *p = &a; 
	修饰int *,指向的内存区域的值是不能通过指针改变。但是指向的地址可改。
	本质是一个指向常量的指针。指针指向的变量的值不可通过该指针修改,但是指针指向的值可以改变。
指向常量的常量指针:	const int * const b = &a;

7、auto

C++98 auto:用于声明变量为自动变量,自动变量意为拥有自动的生命期,这是多余的,因为就算不使用auto声明,变量依旧拥有自动的生命期: auto int b = 20 ; 加不加一样。

C++11 auto:在声明变量的时候根据变量初始值的类型自动为此变量选择匹配的类型,类似的关键字还有decltype。

int a = 10;
auto au_a = a;//自动类型推断,au_a为int类型
cout << typeid(au_a).name() << endl;  // int
  • auto的自动类型推断发生在编译期,所以使用auto并不会造成程序运行时效率的降低。
  • 是否会造成编译期的时间消耗,我认为是不会的。在未使用auto时,编译器也需要得知右操作数的类型,再与左操作数的类型进行比较,检查是否可以发生相应的转化,是否需要进行隐式类型转换。
auto的用法

1、用于代替冗长复杂、变量使用范围专一的变量声明。

不用auto:
std::vector<std::string> vs;
for (std::vector<std::string>::iterator i = vs.begin(); i != vs.end(); i++)

用auto。
for (auto i = vs.begin(); i != vs.end(); i++)
for循环中的i将在编译时自动推导其类型,而不用我们显式去定义

为何不直接使用using namespace std:

2、在定义模板函数时,用于声明依赖 模板参数 的变量类型。

template <typename _Tx,typename _Ty>
void Multiply(_Tx x, _Ty y)
{
    auto v = x*y;
    std::cout << v;
}
由于不知道x*y的真正类型,只能用auto声明v

3、模板函数依赖于模板参数的返回值

template <typename _Tx, typename _Ty>
auto multiply(_Tx x, _Ty y)->decltype(x*y)
{
    return x*y;
}
当模板函数的返回值依赖于模板的参数时,无法在编译代码前确定模板参数的类型,
故也无从知道返回值的类型,这时可以使用auto。

auto在这里的作用也称为返回值占位,它只是为函数返回值占了一个位置,
真正的返回值是后面的decltype(_Tx*_Ty)。

decltype操作符用于查询表达式的数据类型,也是C++11标准引入的新的运算符,
其目的也是解决泛型编程中有些类型由模板参数决定,而难以表示它的问题。

为何要将返回值后置呢?
函数声明若为:decltype(x*y)multiply(_Tx x, _Ty y)
此时x,y还没声明,编译无法通过。
注意事项
  • auto 变量在定义时 必须初始化,这类似于const关键字。

  • 定义在一个auto序列的变量必须始终推导成同一类型。

auto a4 = 10, a5 = 20, a6 = 30;//正确
auto b4 = 10, b5 = 20.0, b6 = 'a';   //错误,b5、b6必须也是int型。
  • 如果初始化表达式是引用,则去除引用语义。
int a = 10;
int &b = a;
auto c = b;   //c不是引用,类型为int而非int&
auto &d = b;  //d是引用,类型为int&
  • 如果初始化表达式为const或volatile(或者两者兼有),则除去const/volatile语义。
const int a1 = 10;
auto  b1= a1; 			//b1不是const,类型为int而非const int
const auto c1 = a1;		//c1是const,类型为const int
  • 如果auto关键字带上&号,则不去除const语意。
const int a2 = 10;
auto &b2 = a2;		//因为auto带上&,故不去除const,b2类型为const int
  • 初始化表达式为数组时,auto关键字推导类型为指针。
int a3[3] = { 1, 2, 3 };
auto b3 = a3; 		// b3为int*
  • 若表达式为数组且auto带上&,则推导类型为数组类型。
int a7[3] = { 1, 2, 3 };
auto & b7 = a7;  	// b7为 int[3]
  • 函数或者模板参数不能被声明为auto。 如 void func(auto a) 错误。

  • auto仅仅是一个占位符,并不是一个真正的类型。不能对auto使用一些以类型为操作数的操作符,如sizeof或者typeid。

8、C++11 decltype关键字

作用:auto必须初始化,当不想初始化时用decltype。选择并返回操作数的数据类型。

  1. 若表达式e指向一个局部变量、命名空间作用域变量、静态成员变量或函数参数,那么返回类型即为该变量(或参数)的“声明类型”;
  2. 若e是一个左值(lvalue,即“可寻址值”),则decltype(e)将返回T&,其中T为e的类型;
  3. 若e是一个x值(xvalue),则返回值为T&&;
  4. 若e是一个纯右值(prvalue),则返回值为T。
decltype用法:

1、基本用法

变量
int a = 2;
decltype(a) b;		// dclTempA为int.

函数
int getsize();		// 可以不定义。decltype只分析,不调用。
decltype(getSize()) dclTempB;   // dclTempB为int。 

2、与const结合

const double a = 5.0;
decltype(a) tmp_a = 4.1; 	// 保留const,类型为const double

const double *const ptr_d = &a;
decltype(ptr_d) d = &a;	// 类型为为const double * const*
cout<<sizeof(d)<<"    "<<*d<<endl;  // size为4(32位计算机),值为5

const b = 6.0;
d = &b;  	// 保留顶层const,不能修改指针指向的对象,编译不过
*d = 7.0; 	// 保留底层const,不能修改指针指向的对象的值,编译不过

3、与引用结合

int a = 0, &ref_a = a;

decltype(ref_a) dcl_a = a;  // dcl_a为引用
decltype(ref_a) dcl_b = 0;	// dcl_b为引用,必须绑定变量,编译不过
decltype(ref_a) dcl_c;		// dcl_c为引用,必须初始化,编译不过
decltype((a)) dcl_d = a; 		// dcl_a为引用。双层括号表示引用。

const int b = 1, &ref_b = b;
decltype(ref_b) dcl_e = a;  // dcl_a为常量引用,绑定普通变量
decltype(ref_b) dcl_f = b;	// dcl_b为常量引用,绑定常量
decltype(ref_b) dcl_g = 0;	// dcl_c为常量引用,绑定临时变量
decltype(a) dcl_h = a; 		// dcl_a为常量引用,必须初始化,编译不过
decltype((a)) dcl_d = b; 	// dcl_a为常量引用。双层括号表示引用。

4、与指针结合

int a = 2;
int *ptr_a = &a; 
decltype(ptr_a) dcl_a;  	// dcl_a为int *指针
decltype(*ptr_a) dcl_b;  	// dcl_b为引用。其中*表示解引用操作,引用必须初始化,编译不过
decltype总结

decltype和auto都可以用来推断类型,但是二者有几处明显的差异:

  1. auto忽略顶层const,decltype保留顶层const;
  2. 对引用操作,auto推断出原有类型,decltype推断出引用;
  3. 对解引用操作,auto推断出原有类型,decltype推断出引用;
  4. auto推断时会实际执行,decltype不会执行,只做分析。总之在使用中过程中和const、引用和指针结合时需要特别小心。

9、C/C++ 中 volatile 关键字

volatile 是一个类型修饰符。

volatile特性

1、易变性:在汇编层面反映出来,就是两条语句,下一条语句不会直接使用上一条语句对应的volatile变量的寄存器内容,而是重新从内存中读取。

变量有时可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。

正常情况下,在代码运行过程中,编译器会优化代码。

  • 前面的指令执行后,将变量结果缓存到寄存器中。之后的指令执行时,编译器直接用寄存器的数据进行计算。

加入volatile 后。编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。

  • 前面的指令执行后,将变量结果缓存到寄存器中,寄存器的结果会被写回内存。之后的指令执行时,编译器重新从内存读取数据,而不再直接使用寄存器的内容。

volatile 可以保证对特殊地址的稳定访问。

2、不可优化性:不要对volatile 变量进行各种激进的优化,甚至将变量直接消除,保证程序员写在代码中的指令,一定会被执行。

void main(){
    int a;
    a = 1;
    printf("%d", a);  // 编译器发现a变量是无用的,将a退化为常量。
		// 对a加入volatile后,变量始终存在,需要从内存读到寄存器中,再调用printf函数。
}

3、顺序性

多线程中简单案例:

volatile bool bStop = FALSE;

线程1:
bStop = TRUE;  
while(bStop);  // 另一个线程通过将bStop复制为False跳出该循环。
// 但是由于bStop已经放置在寄存器中了,如果没有volatile将永远无法跳出。

线程2:
while( !bStop )  {  ...  }  
bStop = FALSE;  
return;  

看起来合理,实际是有错误的。

顺序性案例

案例1:
将 thread1 作为主线程,等待 thread2 准备好 value。
因此,thread2 在更新 value 之后将 flag 置为真,而 thread1 死循环地检测 flag。
简单来说,这段代码的意图希望实现 thread2 在 thread1 使用 value 之前执行完毕这样的语义。
// global shared data
bool flag = false;

thread1() {
    flag = false;
    Type* value = new Type(/* parameters */);
    thread2(value);
    while (true) {
        if (flag == true) {
            apply(value);
            break;
        }
    }
    thread2.join();
    if (nullptr != value) { delete value; }
    return;
}

thread2(Type* value) {
    // do some evaluations
    value->update(/* parameters */);
    flag = true;
    return;
}

问题:
1、在 thread1 中,flag = false 到 while 死循环里,没有任何机会对 flag 的值做修改。
	因此编译器可能会将 if (flag == true) 的内容全部优化掉。
2、在 thread2 中,尽管逻辑上 update 发生在 flag = true 之前,但编译器和 CPU 并不知道。
	因此 flag = true 可能发生在 update 完成之前,
	因此 thread1 执行 apply(value) 时可能 value 还未准备好。

案例2:
将 flag 声明为 volatile。
1、在 thread1 中,flag == true 是对 volatile 变量的访问,故而 if 不会被优化消失。
2、由于value不是volatile,编译器仍有可能将 thread2 中的 update 和对 flag 的赋值交换顺序。
3、由于flag 声明为 volatile,这样使用 volatile 不仅无法达成目的,反而会导致性能下降

案例3:
对 value 也加以 volatile 关键字修饰。注意 value 定义和 thread2 的参数都需要加volatile。
1、都加了volatile,编译器不会交换他们的顺序。
2、代码最终是要运行在 CPU 上,CPU 是乱序执行(out-of-order execution)的。
        在 CPU 执行时,value 和 flag 的赋值仍有可能是被换序了的(store-store)。
3、thread2中,value = new Type()做了三件事:
    分配一块 sizeof(Type) 大小的内存;
    在这块内存上,执行 Type 类型的初始化;
    将这块内存的首地址赋值给 value。
4、对于编译器来说,这些工作都是改表达式语句的求值和副作用,因此不会与 flag 赋值语句换序。
5、在 CPU 乱序执行下,有可能发生 value 和 flag 已赋值完毕,内存里尚未完成 Type 初始化的情况。
6、x86 和 AMD64 架构的 CPU 只允许 sotre-load 乱序,而不会发生 store-store 乱序;
	或者在诸如 IA64 架构的处理器上,对 volatile-qualified 变量的访问采用了专门的指令。
	但是,使用 volatile 会禁止编译器优化相关变量,从而降低性能。
	不建议依赖 volatile 在这种情况下做线程同步。
	此外,这严重依赖具体的硬件规范,不可靠。

案例4:
需要做到的:
一:flag 相关的代码块不能被轻易优化消失。
二:要保证线程同步的 happens-before 语义。
本质上,设计使用 flag 本身也就是为了构建 happens-before 语义。
如有其他不用 flag 的办法解决问题,那么 flag 就不重要。

方法一:使用原子操作
std::atomic<bool> flag = false;  // #include <atomic>
由于对 std::atomic<bool> 的操作是原子的,同时构建了良好的内存屏障。
	因此整个代码的行为在标准下是良定义的。
	
方法二:使用互斥量和条件变量
std::mutex m;                   // #include <mutex>
std::condition_variable cv;     // #include <condition_variable>
bool flag = false;
thread1() {
    flag = false;
    Type* value = new Type(/* parameters */);
    thread2(value);
    std::unique_lock<std::mutex> lk(m);
    cv.wait(lk, [](){ return flag; });
    apply(value);
    lk.unlock();
    thread2.join();
    if (nullptr != value) { delete value; }
    return;
}

thread2(Type* value) {
    std::lock_guard<std::mutex> lk(m);
    // do some evaluations
    value->update(/* parameters */);
    flag = true;
    cv.notify_one();
    return;
}

img

上图为C/C++ Volatile关键词的使用的伪代码。
主要过程:
1、声明一个Volatile的flag变量。
2、Thread1 执行sth操作后,修改flag。Thread2 不断读取flag,基于sth操作,执行other th。
3、flag = true后,能不能保证Thread1中的something操作一定已经完成了?

案例1:非Volatile变量
C/C++编译器最基本优化原理:保证一段程序的输出,在优化前后无变化。
如果编译器改变了A,B变量的赋值顺序,但是foo(A, B)函数的执行结果不变,这么做是可行的,

案例2:一个Volatile变量
变量B被声明为volatile变量。
C/C++ Volatile变量,与非Volatile变量之间的操作,是可能被编译器交换顺序的。

结论: 通过flag = true,来假设something一定完成是不成立的。

案例3:
将A,B两个变量都声明为volatile变量。A,B赋值乱序的现象消失。
C/C++ Volatile变量间的操作,是不会被编译器交换顺序的。

案例:happens-before
由于Thread1中的代码执行顺序发生变化,flag = true被提前到something之前进行。
	整个Thread2的假设全部失效。
能不能将something中所有的变量全部设置为volatile呢?
	CPU本身为了提高代码运行的效率,也会对代码的执行顺序进行调整。
	这就是所谓的CPU Memory Model (CPU内存模型)。
	X86体系(X86,AMD64)也会存在指令乱序执行的行为:
		Store-Load乱序,读操作可以提前到写操作之前进行。
下图构建一个happens-before语义。
	保证Thread1代码块中的所有代码,一定在Thread2代码块的第一条代码之前完成。
	可以使用:Mutex、Spinlock、RWLock等。但C/C++ Volatile关键词不能保证这个语义

img

Volatile:Java增强

Java的Volatile也有这三个特性,但最大的不同在于:第三个特性,”顺序性”,Java的Volatile有很极大的增强——Java Volatile变量的操作,附带了Acquire与Release语义。

1、对于Java Volatile变量的写操作,带有Release语义,所有Volatile变量写操作之前的针对其他任何变量的读写操作,都不会被编译器、CPU优化后,乱序到Volatile变量的写操作之后执行。

2、对于Java Volatile变量的读操作,带有Acquire语义,所有Volatile变量读操作之后的针对其他任何变量的读写操作,都不会被编译器、CPU优化后,乱序到Volatile变量的读操作之前进行。

简而言之:

1、Java Volatile对于编译器、CPU的乱序优化,限制的更加严格了。

2、Java Volatile变量与非Volatile变量的一些乱序操作,也同样被禁止。

3、Java Volatile,能够用来构建happens-before语义。

img

volatile 指针:

const 有常量指针和指针常量的说法,volatile 也有相应的概念

  • (1) 可以把一个非volatile int赋给volatile int,但是不能把非volatile对象赋给一个volatile对象。
  • (2) 除了基本类型外,对用户定义类型也可以用volatile类型进行修饰。
  • (3) C++中一个有volatile标识符的类只能访问它接口的子集,一个由类的实现者控制的子集。用户只能用const_cast来获得对类型接口的完全访问。此外,volatile向const一样会从类传递到它的成员。
什么情况下要用volatile :

1、在内嵌汇编操纵栈时,编译器无法识别的变量改变。

2、多线程并发访问共享变量时,一个线程改变了变量的值,怎样让改变后的值对其它线程 visible。

    1. 中断服务程序中修改的供其它程序检测的变量需要加 volatile;
    1. 多任务环境下各任务间共享的标志应该加 volatile;
    1. 存储器映射的硬件寄存器通常也要加 volatile 说明,因为每次对它的读写都可能由不同意义;

按照 Hans Boehm & Nick Maclaren 的总结volatile 只在三种场合下是合适的。

  • 和信号处理(signal handler)相关的场合;
  • 和内存映射硬件(memory mapped hardware)相关的场合;
  • 和非本地跳转(setjmplongjmp)相关的场合。

10、条件判断nullptr 写前面

比如 if (nullptr == value) 或者 if (nullptr != value)
主要是为了防止少写一个=号。 写出value = nullptr

11、malloc和free

void *malloc(size_t size)
案例:double *p = (double *)malloc(sizeof(double));
注意:返回的是指针,指向在堆区分配的内存空间

void free(void *ptr)
注意:
1、参数是指针,其指向的内存块必须是之前通过调用 malloc、calloc 或 realloc 进行分配内存的。
2、如果传递的参数是一个空指针,则不会执行任何动作。
3、如果指向一个非堆区内存会报warning

12、遍历vector的方法

1、for range
	for (const auto &c : ivec) cout << c << " ";
2、传统for
	for (auto i = 0; i != ivec.size(); ++i) cout << ivec[i] << " ";
3、迭代器    // auto 是 vector::const_iterator
	for (auto it = ivec.cbegin(); it != ivec.cend() ; ++it) cout << *it << " ";
4、for_each + lambda
	for_each(ivec.cbegin(), ivec.cend(), [](const int& c) {cout << c << " "; });
5、ostream_iterator
    ostream_iterator<int> out_iter(cout, " ");
    copy(ivec.cbegin(), ivec.cend(), out_iter);

3.1、template function: 针对别的container也行,比如list等.
	template <typename T>
    void display_container(const T& container) {
      for (auto it = container.cbegin(); it != container.cend(); ++it)
        cout << *it << " ";
	}
	display_container(ivec);

4.1、 for_each + functor
    template <typename elementType>
    struct Display {
      void operator() (const elementType& element) const {
        cout << element << " ";
      }
    };
    for_each(ivec.cbegin(), ivec.cend(), Display<int>());

13、for_each()

for_each()事实上是個 function template

template<typename InputIterator, typename Function>
Function for_each(InputIterator beg, InputIterator end, Function f) {
  while(beg != end) 
    f(*beg++);
}

Object Oriented 与for_each 搭配

14、异或实现加减法

简单来说,异或就是没有进位的加法。
减法的实现原理:计算机是不会做减法的,它是把后一个数变成负数,通过加负数来运算。
	比如3-2就是3+(-2)。
	加法的实现是通过数字的补码相加,这里又涉及原码,反码和补码。
	
原码就是那个二进制编码,比如3就是0000,0011,-3是1000,0011(第一位是符号位)。
反码就是正数不变,负数除了符号位,每一位都反过来,比如3的反码就是本身,-3就变成1111,1100。
补码还是正数不变,负数是反码+1,比如-3就是1111,1101。

接下来讲减法就比较简单了,将两个数的补码相加即可。
	对于3+(-2),就是0000,0011+1111,1110,结果是1,0000,0001。
		前面那个1自动溢出,剩下就是1了。

15、memset函数

在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法
void *memset(void *s, int ch, [size_t];

清零:		memset(a,0,sizeof(a));
无穷大:	memset(a,0x3f,sizeof(a));
最大值:	memset(a,0x7f,sizeof(a)); 第一位为符号位,不能为1,剩下全是1

无穷大为0x3f的原因:
前提:
1、int最大为:2147483647,大概2e9。
2、memset是按照字节操作的,0x3f 在int中就是 0x3f3f3f3f。
原因:
1、0x3f3f3f3f,十进制是1061109567,1e9,一般的数不会比这个大,可以代表无穷。
2、0x3f3f3f3f+0x3f3f3f3f=2122219134。不会爆int,满足:无穷大+无穷大=无穷大
3、memset是按字节操作的,将某个数组全部赋值为无穷大。(解决图论问题时邻接矩阵的初始化)

16、STL 源码分析《2》----nth_element() 使用与源码分析

17、ASCII码转换

'0' 和 '1' 转换:  char c ^= 1;
大小写转换: char c ^= 32;

18、C++读取字符

scanf("%c", &op);  在 %c 前面加空格可以过滤掉空格、tab、换行等。

最好把op协程字符串
char op[2];
scanf("%s", &op);  会自动过滤掉空格、tab、换行等。

19、字符串拷贝

 char *do_strcpy(char *dst, const char *src)
{
	assert(dst != NULL);
	assert(src != NULL);
	char *ret = dst;
	while((* dst++ = * src++) != '\0') // 运算符优先级++高于*
		;
	return ret;
}
1、判断源字符串和目的字符串是否为空
2、现将目的地址指针保存起来,方便输出
3、源字符串const,不能改变
    
考虑字符串重叠情况
#include <bits/stdc++.h>
using namespace std;
char *do_strcpy(char *dest, const char *src){
    assert(dest != NULL);
    assert(src != NULL);
    char *p = dest;
    int count = strlen(src);
    if (dest <= src || dest >= (src + count))
        while ( (*dest++ = *src++) != '\0');
    else{
        char tmp = *src;
        dest += count;
        src += count;
        while ( (*dest-- = *src--) != tmp);
    }
    return p;

}

int main(){
    char a[10];
    char b[] = "1234";
    cout << do_strcpy(a, b) << endl;
}

20、nullptr和NULL

NULL

NULL是一个宏定义,在c和c++中的定义不同,c中NULL为(void*)0,而c++中NULL为整数0。

所以在c++中int *p=NULL; 实际表示将指针P的值赋为0,而c++中当一个指针的值为0时,认为指针为空指针。

//C语言中NULL定义
#define NULL (void*)0      //c语言中NULL为void类型的指针,但允许将NULL定义为0

//c++中NULL的定义
#ifndef NULL
#ifdef _cpluscplus         //用于判定是c++类型还是c类型,详情看上一篇blog
#define NULL 0             //c++中将NULL定义为整数0
#else
#define NULL ((void*)0)    //c语言中NULL为void类型的指针
#endif
#endif

nullptr

nullptr是一个字面值常量,类型为std::nullptr_t,空指针常数可以转换为任意类型的指针类型。

在c++中(void *)不能转化为任意类型的指针,即 int p=(void)是错误的,但int *p=nullptr是正确的,原因

对于函数重载:若c++中 (void *)支持任意类型转换,函数重载时将出现问题下列代码中fun(NULL)将不能判断调用哪个函数

void fun(int i){cout<<"1";};
void fun(char *p){cout<<"2";};
int main()
{
    fun(NULL);  //输出1,c++中NULL为整数0
    fun(nullptr);//输出2,nullptr 为空指针常量。是指针类型
}

21、char* 和 char[]的区别

1、char[] 分配是在栈上,而char* 则分配在字符串常量区

  • 内存分配组成
    1)栈区,由编译器自动释放和分配,存放函数的参数值,局部变量值等。其操作类似与数据结构中的栈
    2)堆一般由程序员自己申请和释放,如果程序员不释放,会在程序结束的时候由系统回收
    3)全局区,全局变量和静态变量存储是存放在一起的,程序结束后由系统释放。
    初始化的全局变量和静态变量存放在一快区域,
    未初始化的全局变量和静态变量存放在一个区域
    4)文字常量区–常量字符串 就是在这里,最后由系统回收
    5)代码区

2、堆和栈的理论知识

  • 申请方式:

    • 栈中申请变量,系统自动分配。块结束时释放。
    • 堆中申请变量,char* p1=(char*)malloc(10); char* p1 = new char(‘a’);
  • 申请后系统响应

    • 栈:只要剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出
    • 堆:
      • 操作系统有一个记录空闲内存地址的链表。当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。
      • 大多数系统,会在这块内存空间中的首地址处记录本次分配的大小。这样,代码中的delete语句才能正确的释放本内存空间。
      • 由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
  • 申请大小限制

    • 栈:栈是向低地址扩展的数据结构,是一块连续的内存区域。栈顶的地址和栈的大小是操作系统预先分配好的,windows上默认大小是2M。如果申请的空间大小溢出,则会提示overflow.因此栈申请空间比堆小。linux用 ulimit -a查看。
    • 堆:是向高地址扩展的数据结构,是不连续的内存区域.显然堆空间的获取比较灵活。
  • 申请效率比较

    • 栈:系统自动分配,速度较快。但程序员是无法控制的。
    • 堆:由new分配的内存,方便,速度较慢,容易产生内存碎片。
  • 堆和栈中的存储内容

    • 栈:在函数调用时,第一个进栈的是主函数中后的下一条指令的地址,然后是函数的各个参数。大多数 C 编译器中,参数是由右往左入栈的,然后是函数中的局部变量。静态变量不入栈。本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。

    • 堆:堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。

  • 存取效率的比较

    • char s1[]:运行时刻赋值的;是一个局部变量,他的值是存放在栈上的所以不允许返回。强行返回的是null,因为函数结束栈上的内容会被全部释放掉。
    • char *s2 : 编译时就确定的;
  • 总结:

    • char[] p:表示p是一个数组指针,相当于const pointer,不允许对该指针进行修改。但该指针所指向的数组内容,是分配在栈上面的,是可以修改的。
    • char *pp:表示pp是一个可变指针,允许对其进行修改,即可以指向其他地方,如pp = p是可以的。对于*pp =“abc”; 编译器会优化,会将abc存放在常量区域内,pp指针是局部变量,存放在栈中。因此,在函数返回中,允许返回该地址

22、int *p = 1;

int *p = 1; 错误。int *p 定义了一个指针p,然而并没有指向任何地址,所以当使用 *p 时是没有任何地址空间对应的,所以 *p=1 就会导致,不知道把这个1赋值给哪个地址空间了。

int *p; p = 1; 错误。 int *p; 定义了一个指针p, p = 1; 意思是将一个存地址为1的地址赋值给p,所以这个是可行的。但是这个操作是不安全的。

23、atoi

int atoi(const char *str)
字符串转换为一个整数,转不了返回0
int val;
char str[20];
strcpy(str, "98993489");
val = atoi(str);  

24、智能指针

动态内存管理经常会出现两种问题:

  • 一种是忘记释放内存,会造成内存泄漏;
  • 一种是尚有指针引用内存的情况下就释放了它,就会产生引用非法内存的指针。

智能指针

#include <memory>
std::unique_ptr -- single ownership
std::shared_ptr -- shared ownership
std::weak_ptr -- temp / no ownership
1、new一个对象指针
2、把指针传给一个函数,函数需要考虑以下问题
	谁 own 对象指针
	对象指针 生命周期多长?
	对象指针是否会被销毁,我要不要销毁
3、如果 函数没有删除的话,指针就泄露了。因为foo结束后,e指针就得不到了。

void func(Entity* e){
}
void foo(){
	Entity* e = new = Entity();
	func(e);
}
foo();

改进:
void func(Entity* e){
	// func own e;
	// e 会自动销毁
}
void foo(){
	auto e = std::make_unique<Entity>();  //创建智能指针
	func(std::move(e));  // 把 ownership 交给 func
}
foo();

unique_ptr 使用方法

std::unique_ptr<Entity> e = new Entity(); // no,不能直接赋值
std::unique_ptr<Entity> e(new Entity()); // yes,调用构函
std::unique_ptr<Entity> e = std::make_unique<Entity>();  // 更好

std::unique_ptr<Entity> e1 = e2; // no,不能赋值
std::unique_ptr<Entity> e1 = std::move(e2); // yes,把ownership给e2,e1变空指针
func(std::move(e));  // 函数参数也不能直接传,要用move递交ownership

shared_ptr 在多线程中,当use_count==0时销毁

std::shared_ptr <Entity> e(new Entity()); // yes,调用构函
std::shared_ptr <Entity> e = std::make_shared<Entity>();  // 更好

std::shared_ptr <Entity> e1 = e2; // 可以拷贝,use_count + 1
std::shared_ptr <Entity> e1 = std::move(e2); // 把ownership给e2,e1变空指针,use_count 不加
func(e);  // 函数参数可以直接传,use_count + 1
func(std::move(e));  // use_count 不加

weak_ptr 很少用

  • 使用前必须转换为 shared_ptr
  • 模拟 temporary ownership
    • 如果还存在就调用
    • 被销毁了也没关系
auto e1 = std::make_shared<Entity>(); 
std::weak_ptr<Entity> ew = e1;
if (std::shared_ptr<Entity> e2 = ew.lock())  # 如果用 use_count ++
	e2 -> func();  // 用完 use_count --

结论:

  1. 尽量用 智能指针 代替 new 和delete
  2. 尽量用 unique_ptr ,而不是shared_ptr ,负载小些
  3. 尽量用move使用shared_ptr ,不用再次拷贝
shared_ptr的线程安全性

总结:引用计数是原子的,但是对象的读写不是

https://blog.csdn.net/D_Guco/article/details/80155323

25、内存对齐

字节对齐主要是为了提高内存的访问效率,比如intel 32位cpu,每个总线周期都是从偶地址开始读取32位的内存数据,如果数据存放地址不是从偶数开始,则可能出现需要两个总线周期才能读取到想要的数据,因此需要在内存中存放数据时进行对齐。

#include <stdio.h>
struct A{
    char a;
    int b;
    short c;
};
  • 在32位机器上char 占1个字节,int 占4个字节,short占2个字节,一共占用7个字节

  • 实际上 12字节, 比计算的7多了5个字节。

内存对齐主要遵循下面三个原则:

  1. 结构体变量的起始地址能够被其 最大字节 整除
  2. 对于结构体的各个成员,第一个成员的 偏移量 是0,排列在后面的成员其当前偏移量必须是当前成员类型的整数倍
  3. 结构体总体大小能够 最大字节 整除,如不能则在后面补充字节

指定大小(默认4)和 自身大小的最小值。默认对齐大小 4 = 最宽数据大小 int 4。

  • 起始地址为0x0000

  • a 1字节,偏移量 1

  • b 4字节,偏移量 1 不能整除 4,增加3位。偏移量 8。

  • c 2字节,偏移量 8,总大小10字节,根据3,补偏移量 2,一共12个字节。

每个特定平台上的编译器都有自己的默认“对齐系数”。可以通过预编译命令#pragma pack(n)

在#pragma pack(1)时,以1个字节对齐时,属于最简单的情况,结构体大小是所有成员的类型大小的和。

26、C语言的编译链接过程?

源代码–>预处理–>编译–>优化–>汇编–>链接–>可执行文件

  1. 预处理
    读取c源程序,对其中的 伪指令(以#开头的指令)和 特殊符号进行处理。包括 宏定义替换、条件编译指令、头文件包含指令、特殊符号等。 预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。 .i预处理后的c文件,.ii预处理后的C++文件。
  2. 编译阶段
    编译程序所要作得工作就是通过 词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其 翻译成等价的中间代码表示或汇编代码。.s文件
  3. 汇编过程
    汇编过程实际上指 把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。 .o目标文件
  4. 链接阶段
    链接程序的主要工作就是 将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够诶操作系统装入执行的统一整体。

扩展: **静态链接和动态链接?**请自查
静态链接:一个目标文件集合;
动态链接:函数代码放到被称作动态链接库或共享对象的某个目标文件中,可执行文件执行时,动态链接库的全部内容映射到运行时相应进程的虚地址空间,动态链接程序根据记录找到对应代码。

27、c++中的引用和指针

引用就是某一变量的一个别名,对引用的操作与对变量直接操作完全一样。引用的声明方法:

指针利用地址,它的值直接指向存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。

区别:

1、指针有自己的一块空间,而引用只是一个别名;

2、使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;

3、指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象的引用;

4、作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;

5、可以有const指针,但是没有const引用;

6、指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变;

7、指针可以有多级指针(**p),而引用至于一级;

8、指针和引用使用++运算符的意义不一样;

9、如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。

28、面向对象(OOP)

面向对象的三大特性:

1、封装
隐藏对象的属性和实现细节,仅对外提供公共访问方式,将变化隔离,便于使用,提高复用性和安全性。
2、继承
提高代码复用性;继承是多态的前提。
3、多态
父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。提高了程序的拓展性。

五大基本原则:

1、单一职责原则SRP(Single Responsibility Principle)
类的功能要单一,不能包罗万象,跟杂货铺似的。

2、开放封闭原则OCP(Open-Close Principle)
一个模块对于拓展是开放的,对于修改是封闭的,想要增加功能热烈欢迎,想要修改,哼,一万个不乐意。

3、里式替换原则LSP(the Liskov Substitution Principle LSP)
子类可以替换父类出现在父类能够出现的任何地方。比如你能代表你爸去你姥姥家干活。哈哈~~

4、依赖倒置原则DIP(the Dependency Inversion Principle DIP)
高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。就是你出国要说你是中国人,而不能说你是哪个村子的。比如说中国人是抽象的,下面有具体的xx省,xx市,xx县。你要依赖的是抽象的中国人,而不是你是xx村的。

5、接口分离原则ISP(the Interface Segregation Principle ISP)
设计时采用多个与特定客户类有关的接口比采用一个通用的接口要好。就比如一个手机拥有打电话,看视频,玩游戏等功能,把这几个功能拆分成不同的接口,比在一个接口里要好的多。

优缺点:

面向过程:

  • 优点:性能比面向对象好,因为类调用时需要实例化,开销比较大,比较消耗资源。
  • 缺点:不易维护、不易复用、不易扩展.

面向对象:

  • 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护 .
  • 缺点:性能比面向过程差

29、析构函数抛出异常

语法上可以,但不能。

析构函数调用:

  • 对象正常结束生命周期时调用,
  • 有异常发生时从函数堆栈清理时调用。

前一种情况抛出异常不会有无法预料的结果,可以正常捕获;

后一种情况下,因为函数发生了异常而导致函数的局部变量的析构函数被调用,析构函数又抛出异常,本来局部对象抛出的异常应该是由它所在的函数负责捕获的,现在函数既然已经发生了异常,必定不能捕获,因此,异常处理机制只能调用terminate()。

若真的不得不从析构函数抛出异常,应该首先检查当前是否有仍未处理的异常,若没有,才可以正常抛出。

当在某一个析构函数中会有一些可能(哪怕是一点点可能)发生异常时,那么就必须要把这种可能发生的异常完全封装在析构函数内部,决不能让它抛出函数之外

30、memcpy()、 memmove()和memccpy()

他们的作用是一样的,唯一的区别是,当内存发生局部重叠的时候,memmove保证拷贝的结果是正确的,memcpy不保证拷贝的结果的正确。

第三个函数的功能也是复制内存,但是如果遇到某个特定值时立即停止复制。

31、虚函数表

虚函数定义:

为什么用虚函数,作用:

32、虚继承

单一继承:class Book : public Library { … };

多重继承:class iostream : public istream , public ostream { … };

虚继承:class Book : virtual public Library { … };

虚继承下,无论基类派生了多少次,只会存在一个实例,称作subobject。

如,iostream中只有virtual ios base class一个实例。 iostream : istream, ostream : ios

33、内联什么时候用?

34、构造函数

拷贝构造函数

构造函数抛出异常

35、#pragma pack(1) 的意义是什么

设置结构体的边界对齐为1个字节,也就是所有数据在内存中是连续存储的。

各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。

比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出,而如果存放在奇地址开始的地方,就可能会需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该int数据。显然在读取效率上下降很多。这也是空间和时间的博弈

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值