41-50==c++知识点

41、auto、decltype和decltype(auto)的用法
  • (1)auto
    • C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。和原来那些只对应某种特定的类型说明符(例如 int)不同auto 让编译器通过初始值来进行类型推演。从而获得定义变量的类型,所以说 auto 定义的变量必须有初始值。举个例子:
      //普通;类型
      int a = 1, b = 3;
      auto c = a + b;// c为int型
      //const类型
      const int i = 5;
      auto j = i; // 变量i是顶层const, 会被忽略, 所以j的类型是int
      auto k = &i; // 变量i是一个常量, 对常量取地址是一种底层const, 所以b的类型是const 
      int*
      const auto l = i; //如果希望推断出的类型是顶层const的, 那么就需要在auto前面加上cosnt
      //引用和指针类型
      int x = 2;
      int& y = x;
      auto z = y; //z是int型不是int& 型
      auto& p1 = y; //p1是int&型
      auto p2 = &x; //p2是指针类型int*
      
  • (2)decltype
    有的时候我们还会遇到这种情况,我们希望从表达式中推断出要定义变量的类型,但却不想用表达式的值去初始化变量。还有可能是函数的返回类型为某表达式的值类型。在这些时候auto显得就无力了,所以C++11又引入了第二种类型说明符decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器只是分析表达式并得到它的类型,却不进行实际的计算表达式的值。
    int func() {return 0};
    //普通类型
    decltype(func()) sum = 5; // sum的类型是函数func()的返回值的类型int, 但是这时不会实际调用函数func()
    
    int a = 0;
    decltype(a) b = 4; // a的类型是int, 所以b的类型也是int
    
    //不论是顶层const还是底层const, decltype都会保留   
    const int c = 3;
    decltype(c) d = c; // d的类型和c是一样的, 都是顶层const
    int e = 4;
    const int* f = &e; // f是底层const
    decltype(f) g = f; // g也是底层const
    
    //引用与指针类型
    //1. 如果表达式是引用类型, 那么decltype的类型也是引用
    const int i = 3, &j = i;
    decltype(j) k = 5; // k的类型是 const int&
    
    //2. 如果表达式是引用类型, 但是想要得到这个引用所指向的类型, 需要修改表达式:
    int i = 3, &r = i;
    decltype(r + 0) t = 5; // 此时是int类型
    
    //3. 对指针的解引用操作返回的是引用类型
    int i = 3, j = 6, *p = &i;
    decltype(*p) c = j; // c是int&类型, c和j绑定在一起
    
    //4. 如果一个表达式的类型不是引用, 但是我们需要推断出引用, 那么可以加上一对括号, 就变成了引用类型了
    int i = 3;
    decltype((i)) j = i; // 此时j的类型是int&类型, j和i绑定在了一起
    
  • (3)decltype(auto)
    decltype(auto)是C++14新增的类型指示符,可以用来声明变量以及指示函数返回类型。在使用时,会将“=”号左边的表达式替换掉auto,再根据decltype的语法规则来确定类型。举个例子:
    int e = 4;
    const int* f = &e;
    decltype(auto) j = f;//j的类型是const int* 并且指向的是e
    
42、public,protected和private访问和继承权限与public/protected/private的区别?
  • public的变量和函数在类的内部外部都可以访问。
  • protected的变量和函数只能在类的内部和其派生类中访问。
  • private修饰的元素只能在类内访问。
  • (一)访问权限
    派生类可以继承基类中除了构造/析构、赋值运算符重载函数之外的成员,但是这些成员的访问属性在派生过程中也是可以调整的,三种派生方式的访问权限如下表所示:注意外部访问并不是真正的外部访问,而是在通过派生类的对象对基类成员的访问。
    在这里插入图片描述
    派生类对基类成员的访问形象有如下两种:
    • 内部访问:由派生类中新增的成员函数对从基类继承来的成员的访问
    • 外部访问:在派生类外部,通过派生类的对象对从基类继承来的成员的访问
  • (二)继承权限
    • public继承
      公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,都保持原有的状态,而基类的私有成员任然是私有的,不能被这个派生类的子类所访问。
    • protected继承
      保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元函数访问,基类的私有成员仍然是私有的,访问规则如下表。
      在这里插入图片描述
    • private继承
      私有继承的特点是基类的所有公有成员和保护成员都成为派生类的私有成员,并不被它的派生类的子类所访问,基类的成员只能由自己派生类访问,无法再往下继承,访问规则如下表。
      在这里插入图片描述
43、如何用代码判断大小端存储
  • 大端存储:字数据的高地址存储在低地址中
  • 小端存储:字数据的低字节存储在低地址中
  • 例如32bit的数字0x12345678
    所以在Socket编程中,往往需要将操作系统所用的小端存储的IP地址转换为大端存储,这样才能进行网络传输
  • 小端模式中的存储方式为:
    在这里插入图片描述
  • 大端模式中的存储方式为:
    在这里插入图片描述
  • 了解了大小端存储的方式,如何在代码中进行判断呢?下面介绍两种判断方式:
    /*方式一、类型转换*/
    #include <iostream>
    using namespace std;
    void main()
    {
    	short int a = 0x1234;
    	char c = char(a);//借助int型转换成char型,高地址截去,保留低地址部分
    	if (c == 0x12)
    		cout << "big endian" << endl;
    	else if (c == 0x34)
    		cout << "little endian" << endl;
    }
    
    /*方式二、union联合体*/
    #include <iostream>
    using namespace std;
    union endian {
    	short int a;
    	char c;
    };
    void main()
    {
    	endian value;
    	value.a = 0x1234;
    	if (value.c == 0x12)
    		cout << "big endian" << endl;
    	else if (value.c == 0x34)
    		cout << "little endian" << endl;
    }
    
44、volatile、mutable和explicit关键字的用法
  • (1) volatile

    • volatile应该解释为“直接存取原始内存地址”比较合适,“易变的”这种解释简直有点误导人
    • **一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去(优化)假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用暂时保存在寄存器里的备份。**下面是volatile变量的几个例子:
      • 1、中断服务程序中修改的供其它程序检测的变量需要加volatile;
      • 2、多任务环境下各任务间共享的标志应该加volatile;
      • 3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;
    • 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。
    • volatile定义变量的值是易变的,每次用到这个变量的值的时候都要去重新读取这个变量的值,而不是读寄存器内的备份。多线程中被几个任务共享的变量需要定义为volatile类型。
    • 嵌入式编程中经常用到 volatile这个关键字,在网上查了下他的用法可以归结为以下两点:
      • 一:告诉compiler不能做任何优化
      比如要往某一地址送两指令:
      int *ip =; //设备地址
      *ip = 1; //第一个指令
      *ip = 2; //第二个指令
      以上程序compiler可能做优化而成:
      int *ip =;
      *ip = 2;
      结果第一个指令丢失。如果用volatile, compiler就不允许做任何的优化,从而保证程序的原意:
      volatile int *ip =;
      *ip = 1;
      *ip = 2;
      即使你要compiler做优化,它也不会把两次付值语句间化为一。它只能做其它的优化。这对device driver程序员很有用。
      
      • 二:表示用volatile定义的变量会在程序外被改变,每次都必须从内存中读取,而不能把他放在cache或寄存器中重复使用。
      volatile char a;
      a=0;
      while(!a){
      //do some things;
      }
      doother();
      如果没有 volatile doother()不会被执行
      
    • 原博客链接
  • (2) mutable

    • mutable的中文意思是“可变的,易变的”,跟constant(既C++中的const)是反义词。
    • 在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。我们知道,如果类的成员函数不会改变对象的状态,那么这个成员函数一般会声明成const的。
    • 但是,有些时候,我们需要在const函数里面修改一些跟类状态无关的数据成员,那么这个
      函数就应该被mutable来修饰,并且放在函数后后面关键字位置。
  • (3) explicit
    **explicit关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显示的方式进行类型转换,**注意以下几点:

    • explicit 关键字只能用于类内部的构造函数声明上
    • explicit 关键字作用于单个参数的构造函数
    • 被explicit修饰的构造函数的类,不能发生相应的隐式类型转换
45、什么情况下会调用拷贝构造函数
  • 用类的一个实例化对象去初始化另一个对象的时候
  • 函数的参数是类的对象时(非引用传递)
  • 函数的返回值是函数体内局部对象的类的对象时 ,此时虽然发生(Named return Value优化)NRV优化,但是由于返回方式是值传递,所以会在返回值的地方调用拷贝构造函数
  • 另:第三种情况在Linux g++ 下则不会发生拷贝构造函数,不仅如此即使返回局部对象的引用,依然不会发生拷贝构造函数
  • 总结就是:即使发生NRV优化的情况下,Linux+ g++的环境是不管值返回方式还是引用方式返回的方式都不会发生拷贝构造函数,而Windows + VS2019在值返回的情况下发生拷贝构造函数,引用返回方式则不发生拷贝构造函数。
  • 在c++编译器发生NRV优化,如果是引用返回的形式则不会调用拷贝构造函数,如果是值传递的方式依然会发生拷贝构造函数。
  • 在VS2019下进行下述实验:
class A
{
public:
	A(){}
	A(const A& a){cout << "copy constructor is called" << endl;}
	~A(){}
};
void useClassA(A a) {}
A getClassA(){//此时会发生拷贝构造函数的调用,虽然发生NRV优化,但是依然调用拷贝构造函数
	A a;
	return a;
}
//A& getClassA2()// VS2019下,此时编辑器会进行(Named return Value优化)NRV优化,不调用拷贝构造函数 ,如果是引用传递的方式返回当前函数体内生成的对象时,并不发生拷贝构造函数的调用
//{
// A a;
// return a;
//}
int main()
{
	A a1,a2,a3,a4;
	A a2 =a1; //调用拷贝构造函数,对应情况1
	useClassA(a1);//调用拷贝构造函数,对应情况2
	a3 = getClassA();//发生NRV优化,但是值返回,依然会有拷贝构造函数的调用 情况3
	a4 = getClassA2(a1);//发生NRV优化,且引用返回自身,不会调用
	return 0;
}
  • 情况1比较好理解
  • 情况2的实现过程是,调用函数时先根据传入的实参产生临时对象,再用拷贝构造去初始化这个临时对象,在函数中与形参对应,函数调用结束后析构临时对象
  • 情况3在执行return时,理论的执行过程是:产生临时对象,调用拷贝构造函数把返回对象拷贝给临时对象,函数执行完先析构局部变量,再析构临时对象, 依然会调用拷贝构造函数
46、C++中有几种类型的new

在c++中,new有三种类型典型的使用方法:plain new,nothrow new和placement new

  • (1) plain new
    言下之意就是普通的new,就是我们常用的new,在c++中定义如下:

    void* operator new(std::size_t) throw(std::bad_alloc);
    void* operator delete(void*) throw();
    

    因此plain new在空间分配失败的情况下,抛出异常常std::bad_alloc而不是返回NULL,因此通过判断返回值是否为NULL是徒劳的,举个例子:

    #include <iostream>
    #include <string>
    using namespace std;
    int main()
    {
    	try{
    		char* p=new char[10e11];//申请字符数组
    		delete p;
    	}
    	catch(const std::bad_alloc& ex){
    		cout<<ex.what()<<endl;
    	}
    	return 0;
    }
    //执行结果:bad allocation
    
  • (2) nothrow new
    nothrow new在空间分配失败的情况下是不抛出异常,而是返回NULL,定义如下:

    void* operator new (std::size_t,const std::nothrow_t&)throw();
    void operator delete(void*) throw();
    

    举个例子:

    #include <iostream>
    #include <string>
    using namespace std;
    int main()
    {
    	char* p=new(nothrow) char[10e11];
    	if(p==NULL)
    		cout<<"alloc failed"<<endl;
    	delete p;
    	return 0;
    }
    //运行结果:alloc failed
    
  • (3) placement new
    这种new允许在一块已经分配成功的内存上重新构造对象或对象数组。placement new不用担心内存分配失败,因为它根本不分配内存,它做的唯一一件事情就是调用对象的构造函数。

    void* operator new(size_t,void*);
    void operator delete(void*,void*);
    
    • 使用placement new需要注意两点:
      • palcement new的主要用途就是反复使用一块较大的动态分配的内存来构造不同类型的对象或者他们的数组
      • placement new构造起来的对象数组,要显式的调用他们的析构函数来销毁(析构函数并不释放对象,需要析构函数内部实现程序),千万不要使用delete,这是因为placement new构造起来的对象或数组大小并不一定等于原来分配的内存大小,使用delete会造成内存泄漏或者之后释放内存时出现运行时错误。
    #include <iostream>
    #include <string>
    using namespace std;
    class ADT{
    	int i;int j;
    public:
    	ADT(){
    		i=10;j=100;
    		cout << "ADT construct i=" << i << ",j="<<j <<endl;}
    	~ADT(){cout << "ADT destruct" << endl;}	
    };
    int main()
    {
    	char* p=new(nothrow) char[sizeof(ADT)+1];
    	if (p == NULL) cout << "alloc failed" << endl;
    	ADT *q = new(p) ADT;//placement new:不必担心失败,只要p所指对象的的空间足够ADT创建即可
    	//delete q;//错误!不能在此处调用delete q;
    	q->ADT::~ADT();//显示调用析构函数(释放前的准备工作)
    	delete[] p;
    	return 0;
    }
    //输出结果:
    //ADT construct i=10,j=100
    //ADT destruct
    
47、C++中NULL和nullptr区别
  • 算是为了与C语言进行兼容而定义的一个问题吧
  • NULL来自C语言,一般由宏定义实现,而 nullptr 则是C++11的新增关键字。在C语言中,NULL被定义为(void*)0,而在C++语言中,NULL则被定义为整数0。编译器一般对其实际定义如下:
    #ifdef _cplusplus
    #define NULL 0
    #else
    #define NULL ((void*)0)
    #endif
    
  • 在C++中指针必须有明确的类型定义。但是将NULL定义为0带来的另一个问题是无法与整数的0区分。因为C++中允许有函数重载,所以可以试想如下函数定义情况:
    #include <iostream>
    using namespace std;
    void fun(char* p){
    	cout<<"char*"<<endl;
    }
    void fun(int p) {
    	cout << "int" << endl;}
    int main()
    {
    	fun(NULL);return 0;
    }
    //输出结果:int,可知c++
    
  • 那么在传入NULL参数时,会把NULL当做整数0来看,如果我们想调用参数是指针的函数,该怎么办呢?。nullptr在C++11被引入用于解决这一问题,nullptr可以明确区分整型和指针类型,能够根据环境自动转换成相应的指针类型,但不会被转换为任何整型,所以不会造成参数传递错误。
  • nullptr的一种实现方式如下:
    const class nullptr_t{
    public:
    	template<class T> inline operator T*() const{return 0;}
    	template<class C,class T> inline operator T C::*() const {return 0;}
    private:
    	void operator&() const;
    } nullptr={};
    
  • 以上通过模板类和运算符重载的方式来对不同类型的指针进行实例化从而解决了(void*)指针带来参数类型不明的问题,另外由于nullptr是明确的指针类型,所以不会与整形变量相混淆。但nullptr仍然存在一定问题,例如:
    #include <iostream>
    using namespace std;
    void fun(char* p) {
    	cout<< "char* p" <<endl; }
    void fun(int* p) {
    	cout<< "int* p" <<endl; }
    void fun(int p) {
    	cout<< "int p" <<endl; }
    int main()
    {
        fun((char*)nullptr);//语句1
     	fun(nullptr);//语句2
        fun(NULL);//语句3
        return 0; 
    }
    //运行结果:
    //语句1:char* p
    //语句2:报错,有多个匹配
    //3:int p
    
  • 在这种情况下存在对不同指针类型的函数重载,此时如果传入nullptr指针则仍然存在无法区分应实际调用哪个函数,默认nullptr是void*,这种情况下必须显示的指明参数类型。
48、简要说明C++的内存分区
  • **C++中的内存分区,分别是堆、栈、自由存储区、全局/静态存储区、常量存储区和代码区。**如下图所示:
    在这里插入图片描述
  • :在执行函数时,函数内部局部变量的存储单元都可以再栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  • :就是那些由 new 分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new 就要对应一个 delete 。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收
  • 自由存储区域:就是那些由malloc等分配的内存块,它和堆是十分相似的,不过他是用free来结束自己的生命的。
  • 全局/静态存储区:全局变量和静态变量被分配到同一块内存中,在以前的c语言中,全局变量和静态变量又分为初始化和未初始化的,在c++里面没有这个区分,他们共同占用同一块内存区,在该区定义的变量若没有初始化,则会被自动初始化,例如int型变量自初始化0。
  • 常量存储区域:这是一块比较特俗的存储区,这里存放着常量,不允许修改
  • 代码区:存放函数体的二进制代码
49、C++的异常处理的方法
  • 在程序执行过程中,由于程序员的疏忽或是系统资源紧张等因素都有可能导致异常,任何程序都无法保证绝对的稳定,常见的异常有:
    • 数组下标越界
    • 除法计算时除数为0
    • 动态分配空间时空间不足

    • 如果不及时对这些异常进行处理,程序多数情况下会崩溃
  • (1) try、throw和catch关键字
    C++中的异常处理机制主要使用try、throw和catch三个关键字,其在程序中的用法如下:
    #include <iostream>
    using namespace std;
    int main()
    {
    	double m=1,n=0;
    	try{
    		cout<<"before dividing."<<endl;
    		if(n==0) thorw-1;//抛出int型异常
    		else if(m==0)
    			throw-1.0;//抛出double型异常
    		else
    			 cout << m / n << endl;
    		cout<<"after diving"<<endl;
    	}
    	catch(double d)
    		cout << "catch (double)" << d << endl;
    	catch(...)
    		cout << "catch (...)" << endl;
    	 cout << "finished" << endl;
    	 return 0;
    }
    //运行结果
    //before dividing.
    //catch (...)
    //finished
    
    代码中,对两个数进行除法计算,其中除数为0。可以看到以上三个关键字,程序的执行流程是先执行try包裹的语句块,如果执行过程中没有异常发生,则不会进入任何catch包裹的语句块,如果发生异常,则使用throw进行异常抛出,再由catch进行捕获,throw可以抛出各种数据类型的信息,代码中使用的是数字,也可以自定义异常class。catch根据throw抛出的数据类型进行精确捕获(不会出现类型转换),如果匹配不到就直接报错,可以使用catch(…)的方式捕获任何异常(不推荐)。当然,如果catch了异常,当前函数如果不进行处理,或者已经处理了想通知上一层的调用者,可以在catch里面再throw异常。
  • (2)函数的异常声明列表
    有时候,程序员在定义函数的时候知道函数可能发生的异常,可以在函数声明和定义时,指出所能抛出异常的列表,写法如下:
    int fun() throw(int,double,A,B,C){...};
    这种写法表名函数可能会抛出int,double型或者A、B、C三种类型的异常,如果throw中为空,表明不会抛出任何异常,如果没有throw则可能抛出任何异常
  • (3)C++标准异常类 exception
    • C++ 标准库中有一些类代表异常,这些类都是从 exception 类派生而来的,如下图所示
      在这里插入图片描述
    • bad_typeid:使用typeid运算符,如果其操作数是一个多态类的指针,而该指针的值为 NULL,则会拋出此异常,例如:
      #include <iostream>
      #include <typeinfo>
      using namespace std;
      class A{
      public:
        virtual ~A();
      };
      
      using namespace std;
      int main()
      {
      	A* a=NULL;
      	try{
      	cout << typeid(*a).name() << endl; // Error condition
      	}
      	catch(bad_typeid){
      		cout << "Object is NULL" << endl;
      	}
      	return 0;
      }
      //运行结果:bject is NULL
      
    • bad_cast:在用** dynamic_cast 进行从多态基类对象(或引用)到派生类的引用的强制类型转换时**,如果转换是不安全的,则会拋出此异常
    • bad_alloc:在用 new 运算符进行动态内存分配时,如果没有足够的内存,则会引发此异常
    • out_of_range:用 vector 或 string的at 成员函数根据下标访问元素时,如果下标越界,则会拋出此异常
50、static的用法和作用?
  • 1.先来介绍它的第一条也是最重要的一条:隐藏。(static函数,static变量均可).
    同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。
  • 2.static的第二个作用是保持变量内容的持久。**(static变量中的记忆功能和全局生存期)**存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。
  • 3.static的第三个作用是默认初始化为0(static变量)(c++,c是分为初始化化与为初始化)
    其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量。
  • 4.static的第四个作用:C++中的类成员声明static
      1. 函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
      1. 模块内的static全局变量*可以被模块内所用函数访问,但不能被模块外其它函数访问;
      1. 模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;
      1. 类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;
      1. 类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。
    • 类内=======================================
      1. static类对象必须要在类外进行初始化,static修饰的变量先于对象存在,所以static修饰的变量要在类外初始化;
      1. 由于static修饰的类成员属于类,不属于对象,因此static类成员函数是没有this指针的,this指针是指向本对象的指针。正因为没有this指针,所以**static类成员函数不能访问非static的类成员,**只能访问static修饰的类成员;
      1. static成员函数不能被virtual修饰,static成员不属于任何对象或实例,所以加上virtual没有任何实际意义;**静态成员函数没有this指针,**虚函数的实现是为每一个对象分配一个vptr指针,而vptr是通过this指针调用的,所以不能为virtual;虚函数的调用关系,this->vptr->ctable->virtual function
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

栋哥爱做饭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值