C++复习

文章目录

指针和引用的区别:
  • 指针是一个变量,存储的是一个地址;引用和原本的变量实质是一个东西,是原变量的别名
  • 指针可以有多级,但引用只有一级
  • 指针可以为空,但引用不能为空
  • 指针在初始化之后可以改变指向,但引用初始化之后不能改变
  • sizeof指针得到是指针本身的大小,sizeof引用得到的是所指变量的大小
  • 当把指针作为参数进行传递时,也是将实参的一个拷贝传递给形参,两者指向的地址相同,但不是同一个变量,在函数中改变这个变量的指向不影响实参,而引用却可以。
  • 不存在指向空值的引用,但存在指向空值的指针
在传递参数时,什么时候改使用指针,什么时候改使用引用?
  • 在需要返回函数局部变量的内存时使用指针,使用指针传参需要开辟内存,用完记得要释放,不然会内存泄漏,,而返回局部变量是没有意义的
  • 对栈空间大小比较敏感(如递归)的时候使用引用,使用引用传递不需要创建临时变量,开销要更小。
  • 类对象作为参数传递时使用引用,C++类对象传递的标准方式
堆和栈
  • 申请方式不同
    • 栈有系统自动分配
    • 堆是自己申请和释放的
  • 申请大小限制不同
    • 栈顶和栈底是之前预设好的,栈是向栈底扩展,大小固定。
    • 堆向高地址扩展,是不连续的内存区域,大小可以灵活调整
  • 申请效率不同
    • 堆由系统分配,速度快,不会有碎片
    • 堆由程序员分配,速度慢,且有碎片
  • 栈空间默认是4M,堆区一般是1G~4G
  • 分配方式不同
    • 堆是动态分配的
    • 栈由静态分配(如局部变量分配),也有动态分配(alloca函数)
  • 栈比堆快,因为操作系统会在底层堆栈提供支持,会分配专门的寄存器存放栈的地址。而堆的操作使用C++库函数提供的,在分配内存时需要一定的算法寻找合适大小的内存,并获取堆内容需要两次访问,一次访问指针,第二次根据指针保存的地址访问内存,因此堆比较慢。
指针数组与数组指针
int *p[10];
int (*p)[10];
int *p(int);
int (*p)(int);
  • int *p[10]表示数组指针,强调数组概念,是一个数组变量,数组大小为10,数组内每个元素都是指向int类型的指针变量。
  • int (*p)[10]表示数组指针,强调是指针,只有一个变量,是指针类型,不过指向的是int类型的数组,这个数组大小是10
  • int * p(int) 是函数声明,函数名是p,参数树int类型的,返回值是 int*类型的
  • int (*p)(int) 是函数指针,强调是指针,该指针指向的函数具有int类型参数,并且返回值是int类型的。
new/delete与malloc/free的异同
  • 相同点:

    • 都可以用于内存的动态申请与释放
  • 不同点:

    • new与delete是C++运算符,后置是C/C++语言标准库函数

    • new自动计算要分配的空间大小,malloc需要手动计算

    • new是类型安全的,malloc不是

      int *p=new float[2];//编译报错
      int *p=(int*)malloc(2*sizeof(double));//编译无报错
      
    • new调用名为operator new的标准库函数分配足够的空间并调用相关的构造函数,delete对指针所指向的对象运行适当的析构函数;然后通过调用名为operator delete的标准库函数释放该对象所用内存,后者均没有相关调用。

    • 后者需要库文件支持,前置不会

    • new是封装了malloc,直接free不会报错,但这只是释放内存,并不会调用析构函数

  • new和delete是如何实现的?

    • new的实现过程是:首先调用名为operator new的标准库函数,分配足够大的原始为类型化的内存,以保存指定类型的一个对象,接下来运行该类型的一个构造函数,用指定的初始化构造对象;最后返回指向新分配并构造后的对象的指针
    • delete的实现过程:对指针指向的对象运行适当的析构函数;然后通过调用名为operator delete的标准库函数释放该对象所用内存
  • malloc与new的区别:

    • malloc和free都是标准库函数,支持覆盖,new与delete是运算符,支持重载。
    • malloc仅仅分配内存空间,free仅仅回收空间,不具备调用构造函数与析构函数,用malloc分配存储类的对象存在风险,new和delete除了分配回收功能外,还会调用构造函数与析构函数。
    • malloc和free返回的是void类型的指针(必须要进行类型转换),new与delete返回的是具体类型的指针
宏定义与typedef区别
  • 宏主要用于定义常量及书写复杂的内容;typedef主要用于定义类型别名。
  • 宏替换发生再编译阶段之前,输入文本插入替换;typedef是编译的一部分
  • 宏不检查类型;typedef会检查数据类型
  • 宏不是语句,不在最后加分号;typedef是语句,要加分号标识结束。
宏定义和函数的区别:
  • 宏再预处理阶段完成替换,之后被替换的文本参与编译,相当于直接插入了代码,运行时不存在函数调用,执行起来更快;函数调在运行时需要跳转到具体调用函数
  • 宏定义属于在结构体中插入代码,没有返回值;函数调用具有返回值
  • 宏定义参数没有类型,不进行类型检查;函数参数具有类型,需要检查类型
  • 宏定义不要再最后加分号
strlen与sizeof的区别
  • sizeof是运算符,并非是函数,结果再编译时得到,并非运行中获得;strlen是字符处理的库函数

  • sizeof参数可以是任何数据类型或者数据,strlen的参数只能是字符指针且结尾为’\0’的字符串

  • 因为sizeof值在编译时确定,所以不能用来得到动态分配存储空间的大小

    int main()
    {
    	const char*str="name";
    	sizeof(str);//取得是指针str的长度,是8
    	strlen(str);//取得是这个字符串的长度 不包含结尾的'\0'
    	
    	return 0;
    }
    
指针常量和常量指针
  • int const* p||const int *p:指针常量是一个指针,读成常量的指针,指向一个只读变量,也就是后面所指明的int const和const int 都是一个常量。
  • int *const p常量指针是一个不可改变它指向的指针。指针是一个常量,必须初始化,一旦初始化完成,它的值就不可再改变。
a和&a有什么区别?

假设数组int a[10],int (*p)[10]=&a;其中

  • a是数组名,是首元素地址,+1表示地址值加上一个int类型的大小,如果a的值是0x00000001,加1操作后编程0x0000005,*(a+1)=a[1];
  • &a就是数组的指针,其类型为int(*)[10]
C++和C语言的区别:
  • C++中的new和delete是对内存分配的运算符。取代了C中的malloc和free
  • 标准C++中的字符串类取代了标准C函数库头文件中的字符数组处理函数
  • C++中用来做控制态输入输出的iostream类库代替了标准C中的stdio函数库
  • C++中的try/catch/throw异常处理机制取代了标准C中的setjump()和longjmp()函数
  • 在C++中允许有咸通的函数名,不过他们的参数类型不能完全相同,这样函数就可以相互区分开。而C语言中是不允许的,也就是说C++可以重载,但C语言不可以。
  • C++中,允许变量定义语句在程序中的任何地方,只要是在使用它之前就可以;而C语言中,必须要放在函数开头部分。
  • C++中,除了值和指针之外,新增了引用。引用型变量是其他变量的一个别名,我们可以认为他们只是名字不同,其他都是相同的。
  • C++相对C增加了一些关键字,如bool,using,namespace…
C++中struct和class

相同点

  • 两者都拥有成员函数,公有和私有部分
  • 任何可以使用class完成的工作,同样可使用struct完成

不同点

  • 两者中如果不指定公私有,struct默认是公有的,class则默认是私有的
  • class默认是private继承,而struct默认是public继承

引申

  • C语言中:struct是用户自定义类型数类型,且C++中struct是抽象数据类型,支持成员函数的定义
  • C中的struct是没有权限的设置的,且struct中只能是一些变量的集合体,可以封装数据但不能隐藏数据,而且成员不可以是函数

函数

  • C++中,struct增加了访问权限,且可以和类一样有成员函数,成员默认访问说明符为public
  • struct作为类的一种特例是用来自定义数据结构的。一个结构s标记声明后,在C中必须在结构标记前加上struct,才能做到结构类型名,C++中结构体标记可以直接作为结构体类型名使用,此外结构体struct在C++中被当作类的一种特例
define宏定义和const的区别

编译阶段

  • define实在编译的预处理阶段起作用的,而const是在编译,运行的时候起作用的

安全性

  • define只做替换,不进行类型检查和计算,也不求解,容易产生错误,一般最好加上一个大括号包含住全部内容,要不然很容易出现错误
  • const常量有数据类型,编译器会对其进行类型安全检查

内存占用

  • define只是将宏名称进行替换,在内存中会产生多份相同的备份,const在程序运行中只有一份备份,且可以执行常量折叠,能将复杂的表达式计算出结果放入常量表
  • 宏替换发生在编译阶段,属于文本插入替换;而const作用发生在编译过程中
  • 宏不检查类型,const会检查数据类型
  • 宏定义的数据没有分配内存空间,只是插入替换掉,const定义的变量只是值不能改,但需要分配内存空间
C++中的const和static的作用

static

  • 不考虑类的情况
    • 隐藏。所有不加static的全局变量和函数具有全局可见性,可以在其他文件中使用,加了之后只能在该文件所在编译模块中使用
    • 默认初始化为0,包括为初始化的全局静态变量与局部静态变量,都存在未初始化区
    • 静态变量在函数内定义,始终存在,且只进行一次初始化,具有记忆性,起作用范围与局部变量相同,函数退出后仍然存在,但是不能使用
  • 考虑类的情况
    • static成员变量:只与类关联,不与类的对象关联。定义时要分配空间,不能再类声明中初始化,必须再类定义体外部初始化,初始化时不需要标示static,可以被非static成员函数任意访问
    • static函数:不具有this指针,无法访问类对象的非static成员变量和非static成员函数,不能被声明为const,虚函数和volatile,可以被非static成员函数任意访问

const

  • 不考虑类的情况
    • const常量在定义时,必须初始化,之后无法更改
    • const形参可以接受const和非const类型的实参
  • 考虑类的情况
    • const成员变量:不能在类定义外部初始化,只能通过构造函数初始化列表进行初始化,并且必须有构造函数,不同类对其const数据成员的值可以不同,所以不能在类中声明时初始化
    • const成员函数:const对象不可以调用非const成员函数,非const对象都乐意调用,不可以改变mutable数据的值
数组名和指针
  • 两者均可以通过增减偏移量来访问数组中的元素
  • 数组名不是真正意义上的指针,可以理解为常指针,所以数组名没有自增,自减等操作
  • 当数组名当作形参传递给调用函数后,就失去了原有特性,退化成一般指针,多了自增,自减操作,但sizeof运算符不能再得到原数组的大小了
拷贝初始化和直接初始化
  • 当用于类类型对象时,初始化的拷贝形式有所不同,直接初始化直接调用与实参匹配的构造函数,拷贝初始化总是调用拷贝构造函数,拷贝初始化首先使用指定构造函数创建一个临时对象,然后用拷贝构造函数将那个临时对象拷贝到正在创建的对象:举例如下

    string str1("I am a string!");//直接初始化
    string str2(str1);//直接初始化,str1是已经存在的对象,直接调用拷贝构造函数对str2进行初始化
    string str3="I am a string!"//拷贝初始化,先对字符串创建临时对象,再把临时对象作为参数,使用拷贝构造函数构造str3
    string str4=str1;//拷贝构造函数,相当于隐式调用拷贝构造函数,而不是运算符重载
    
  • 为提高效率,允许编译器跳过创建临时对象这一步,直接调用构造函数要创建的对象,这样就完全等价于直接初始化了(str1和str3等价),但需要辨别两种情况

    • 当拷贝构造函数为private时,str3于str4在编译时会报错
    • 使用explicit修饰构造函数时,如果构造函数存在隐式转化,编译器就会报错。
初始化和赋值的区别:
  • 对于简单类型来说,初始化和赋值并无区别
  • 对于类和复杂数据类型来说,区别较大
class A{
public:
    int num1;
    int num2;
public:
    A(int a=0, int b=0):num1(a),num2(b){};
    A(const A& a){};
    //重载 = 号操作符函数
    A& operator=(const A& a){
        num1 = a.num1 + 1;
        num2 = a.num2 + 1;
        return *this;
    };
};
int main(){
    A a(1,1);
    A a1 = a; //拷贝初始化操作,调用拷贝构造函数
    A b;
    b = a;//赋值操作,对象a中,num1 = 1,num2 = 1;对象b中,num1 = 2,num2 = 2
    return 0;
}

野指针和悬空指针

都是访问指向无效内存区域的指针,访问行为将会导致未定义行文

  • 野指针:

    指的是没有被初始化过的指针

    int main()
    {
    	int *p;//为初始化的
    	cout<<*p<<endl;
    	return 0;
    }
    

    因此,为了防止出错,对指着初始化时都是赋值为nullptr,这样在使用时编译器就不会直接报错,产生非法内存访问

  • 悬空指针

    是指最初指向的内存已经被释放了的一种指针

    int main()
    {
    	int *p=nullptr;
    	int *p2=new int;
    	
    	p=p2;
    	delete p2;
    }
    

    此时,p和p2就是悬空指针,指向的内存被释放,继续使用这两个指针,行为不可预料,因此要设置p=p2=nullptr;此时在使用,编译器就不会直接报错。避免野指针比较简单,但悬空指针就比较麻烦,C++引入智能指针,C++智能指针的本质就是避免悬空指针的产生

    产生原因以及解决方法

    野指针:指针变量未及时初始化,定义指针变量及时初始化,要么置空

    悬空指针:指针free或者delete之后及时置空,释放操作后立即置空

C++中的重载,重写和隐藏的区别
  1. 重载(overload)

    重载是指在同一范围定义中的同名的成员函数才存在重载关系,主要特点时函数名相同,参数类型或者数目不同,不能出现参数类型和个数相同,仅依靠返回值不同来区分的函数,重载和函数成员是否为虚函数无关。

    class A
    {
    	...
    	virtual int fun();
    	void fun(int);
    	void fun(double,double);
    	static int fun(char);
    	...
    };
    
  2. 重写(覆盖)(override)

    重写是指在派生类中覆盖基类中的同名函数,重写就是重写函数体,要求基类函数必须是虚函数且:

    • 与基类的虚函数有相同的参数个数
    • 与基类的虚函数有相同的参数类型
    • 与基类的虚函数有相同的返回值
    //父类
    class A{
    public:
    	virtual int fun(int a){}
    };
    //子类:
    class b:public A
    {
    public:
    	//重写一般加override可以确保时重写父类的函数
    	virtual int fun(int a) override{}
    };
    
    

    重载和重写的区别:

    • 重写是父类和子类之间的垂直关系,重载是不同函数之间的水平关系
    • 重写要求参数列表相同,重载则要求参数列表不同,返回值不做要求
    • 重写关系中,调用方法根据对象类型决定,重载根据调用时实参与形参表的对应关系来选择函数体
  3. 隐藏(hide)

    隐藏指默写情况下,派生类类中的函数屏蔽了基类中的同名函数包括一下情况:

    • 两个函数参数相同,但是基类函数不是虚函数。和重写的区别就在于基类函数是否是虚函数。

      //父类:
      class A{
      public:
      	void func(int a)
      	{
      		cout<<"A::func()"<<endl;
      	}
      };
      //子类
      class B:public A
      {
      public:
      	//隐藏父类的func函数
      	void func(int a)
      	{
      		cout<<"B::func()"<<endl;
      	}
      };
      
      int main()
      {
      	B b;
      	b.func(2);//调用B::func()函数
      	b.A::func(2);//调用A::func()函数
      	return 0;
      }
      
    • 两个函数参数不同,无论基类函数是不是虚函数,都会被隐藏,和重载的区别在于:两个函数不在同一个类中

      //父类
      class A{
      public:
          virtual void fun(int a){
      		cout << "A中的fun函数" << endl;
      	}
      };
      //子类
      class B : public A{
      public:
          //隐藏父类的fun函数
         virtual void fun(char* a){
      	   cout << "A中的fun函数" << endl;
         }
      };
      int main(){
          B b;
          b.fun(2); //报错,调用的是B中的fun函数,参数类型不对
          b.A::fun(2); //调用A中fun函数
          return 0;
      }
      
      

      补充

      // 父类
      class A {
      public:
          virtual void fun(int a) { // 虚函数
              cout << "This is A fun " << a << endl;
          }  
          void add(int a, int b) {
              cout << "This is A add " << a + b << endl;
          }
      };
      
      // 子类
      class B: public A {
      public:
          void fun(int a) override {  // 覆盖
              cout << "this is B fun " << a << endl;
          }
          void add(int a) {   // 隐藏
              cout << "This is B add " << a + a << endl;
          }
      };
      
      int main() {
          // 基类指针指向派生类对象时,基类指针可以直接调用到派生类的覆盖函数,也可以通过 :: 调用到基类被覆盖
          // 的虚函数;而基类指针只能调用基类的被隐藏函数,无法识别派生类中的隐藏函数。
      
          A *p = new B();
          p->fun(1);      // 调用子类 fun 覆盖函数
          p->A::fun(1);   // 调用父类 fun
          p->add(1, 2);
          // p->add(1);      // 错误,识别的是 A 类中的 add 函数,参数不匹配
          // p->B::add(1);   // 错误,无法识别子类 add 函数
          return 0;
      }
      
浅拷贝和深拷贝

浅拷贝

浅拷贝只是拷贝一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,如果原来的指针所指向的资源释放了,那么在释放浅拷贝的指针的资源就会出现错误

深拷贝

深拷贝不仅拷贝值,还开辟一块新空间用来存放新的值,即使原先的对象被析构掉,释放内存了也不会影响到深拷贝得到的值,在自己实现拷贝赋值的时候,如果有指针变量的话,是需要自己实现深拷贝的

#include<iostream>
#include<string>
using namespace std;
class Student
{
private:
	int num;
	char *name;
public:
	Student(){
		name=new char(20);
		cout<<"Student"<<endl;
	}
	~Student()
	{
        cout<<"~Student "<<&name<<endl;
        delete name;
        name=NULL;
	}
	Student(const Student&s)//拷贝构造函数
	{
		//浅拷贝:当对象的name和传入的对象的name指向相同的地址空间
		name=s.name;
		
		//深拷贝
		name=new char(20);
		memcpy(name,s.name,strlen(s.name));
		cout<<"Copy Student "<<endl;
	}
};
int main()
{
	{// 花括号让s1和s2变成局部对象,方便测试
		Student s1;
		Student s2(s1);// 复制对象
	}
	system("pause");
	return 0;
}
//浅拷贝执行结果:
//Student
//copy Student
//~Student 0x7fffed0c3ec0
//~Student 0x7fffed0c3ed0
//*** Error in `/tmp/815453382/a.out': double free or corruption (fasttop): 0x0000000001c82c20 ***

//深拷贝执行结果:
//Student
//copy Student
//~Student 0x7fffebca9fb0
//~Student 0x7fffebca9fc0

从执行结果可以看出,浅拷贝在对象的拷贝创建时存在风险,即被拷贝的对象析构释放资源后,拷贝对象析构时会再次释放一个释放过的资源,深拷贝的结构是两个对象之间没有任何关系,各自成员地址不同。

public,protected和private访问和继承权限public,protected,private的区别
  • public的变量和函数在类的内部外部都可以访问
  • protected的变量和函数只能在类的内部和其派生类中访问
  • private修饰的元素只能在类内访问

一、访问权限

访问权限外部派生类内部
public
protected
private

public、protected、private 的访问权限范围关系:

public > protected > private

二、继承权限

  1. 派生类继承自基类的成员权限有四种状态:public、protected、private、不可见
  2. 派生类对基类成员的访问权限取决于两点:一、继承方式;二、基类成员在基类中的访问权限
  3. 派生类对基类成员的访问权限是取以上两点中的更小的访问范围(除了 private 的继承方式遇到 private 成员是不可见外)。例如:
  • public 继承 + private 成员 => private
  • private 继承 + protected 成员 => private
  • private 继承 + private 成员 => 不可见
如何用代码判断大小端存储

大段存储:字数据的高字节存储在低地址中

小端存储:字数据的低字节存储在低地址中

例如:32bit数字0x12345678

所以在Socket编程中,往往需要将操作系统所用的小端存储的IP地址转换为大端存储,这样才能进行网络传输

小端模式的存储方式:

内存地址0x40000x40010x40020x4003
存放内容0x780x560x340x12

大端模式的存储方式:

内存地址0x40000x40010x40020x4003
存放内容0x120x340x560x78

判断方式:

方式一:使用强制类型转换

#include<iostream>
using namespace std;

int main()
{
	int a=0x1234;
    //由于int和char的长度不同,借助int类型转换为char型,只会留下低地址的部分
	char c=(char)a;
	if(c==0x12)
		cout<<"big endian"<<endl;
	if(c==0x34)
		cout<<"small endian"<<endl;
}

方式二:使用union联合体

#include <iostream>
using namespace std;
//union联合体的重叠式存储,endian联合体占用内存的空间为每个成员字节长度的最大值
union endian
{
    int a;
    char ch;
};
int main()
{
    endian value;
    value.a = 0x1234;
    //a和ch共用4字节的内存空间
    if (value.ch == 0x12)
        cout << "big endian"<<endl;
    else if (value.ch == 0x34)
        cout << "little endian"<<endl;
}
volatile,mutable和explicit关键字的用法
  1. volatile

    volatile关键字是一种类型修饰符,**用它声明的类型变量表示可以被某些编译器未知的因素更改,**比如,操作系统,硬件或者其他线程等,遇到这个关键字声明的变量,编译器对访问改变量的代码就不在优化,从而可以提供对特殊地址的稳定访问。

    当要求使用volatile声明的变量的值的时候**,系统总是重新从它所在的内存读取数据,**即使它前面的指令刚刚从该处读取过数据。

    volatile定义变量的值是易变的,每次用到这个变量的值的时候都要去重新读取这个变量的值,而不是读寄存器内的备份。多线程中被几个任务共享的变量需要定义未volatile类型。

  2. mutable

    mutable的中文是"可变的,易变的",跟constant(即C++中的const)是反义词。在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可突破状态,即使在一个const函数中,我们知道,如果类的成员函数不会改变对象的状态,那么这个成员函数一般会被声明为const,但是,有时候我们需要在const函数里面修改一些与类状态无关的数据成员,那么这个函数就应被mutable来修饰,并放在函数后面关键字位置。

    class Person
    {
    	int m_A;
    	mutable int m_B;//特殊变量,在常函数里值也可以被修改
    public:
    	void add() const//在函数里不可修改this指针指向的值 常量指针
    	{
    		m_A=10;//错误,不可修改值,this指针已经被修改未常量指针
    		m_B=20;//正确
    	}
    	
    };
    class person
    {
    int m_A;
    mutable int m_B;//特殊变量 在常函数里值也可以被修改
    }
    int main()
    {
    const person p;//修饰常对象 不可修改类成员的值
    p.m_A=10;//错误,被修饰了指针常量
    p.m_B=200;//正确,特殊变量,修饰了mutable
    };
    
  3. explicit

    explicit关键字用来修饰变量的构造函数,被修饰的构造函数的类,不能发生隐式类型转换,只能以显示的方式进行类型转换,注意一下几点:

    • explicit关键字只能用于类内部的构造函数声明上
    • explicit关键字作用于单个参数的构造函数
    • 被explicit修饰的构造函数的类,不能发生相应的隐式类型转换
C++中有几种类型的new

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

  1. plain new

    言下之意就是普通new,就是我们平时使用的new,plain new在空间分配失败的情况下,抛出异常std::bad_alloc而不是返回NULL,因此通过返回值判断是否为NULL是徒劳的。

  2. nothrow new

    nothrow new在空间分配失败的情况下是不抛出异常的,而是返回NULL

  3. placement new

    这种new允许在一块已经分配成功的内存上重新构建构造对象或对象数组。placement new不必担心内存分配失败,因为它根本步分配内存,它唯一做的就是调用对象的构造函数。

static的用法和作用
  1. 最重要的一条:隐藏。(static函数,static变量均可)

    当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性

  2. 保持变量内容的持久。(static变量中的记忆功能和全局生存期)存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。

  3. 默认初始化为0

  4. C++中类成员声明static

    1. 函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值
    2. 在模块内的static全局变量可以被模板内所有函数访问,但不能被模块其他函数访问。
    3. 在模块内的static函数只可被这一模块内的其他函数调用,这个函数的适用范围被限制在声明他的模块内。
    4. 类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝
    5. 在类中的static成员函数属于整个类所拥有,这个函数不接受this指针,因此只能访问类的static成员变量
    6. static修饰的类成员属于类,不属于对象,因此static类成员函数是没有this指针的,this指针是指向本对象的指针,正因为没有this指针,所以static类成员函数不能访问非static成员函数,只能访问static修饰的类成员
    7. static成员函数不能被vritual修饰,static成员不属于任何对象或实例,所以加上vritual没有任何实际意义;静态成员函数没有this指针,虚函数的实现是为每一个对象分配一个vptr指针,而bptr是通过this指针调用的,所以不能为vritual,虚函数第调用关系:this->vptr->ctable->virtual->function
指针和const的用法
  1. 当const修饰指针时,由于const的位置不同,它的修饰对象会有不同
  2. int *const p2中,const修饰p2的值,所以p2的值不可改变,即p2只能指向固定的一个地址,但可以通过 *p读写这个变量来修改这个变量的值。顶层指针表示指针本身是一个常量
  3. int const *p/const int *p两种情况中,const修饰 *p,所以理解为 *p的值不可以改变,即不可以给 *p赋值改变p指向变量的值,但可以通过修改p赋给不同的地址改变这个指针指向
形参和实参的区别
  1. 形参只有在被调用时才分配内存单元,在调用结束后,即刻释放所分配的内存单元。因此,形参只有在函数内部有效。函数调用结束后返回主函数后不在使用该形参变量
  2. 实参可以是常量,变量,表达式,函数等,无论是实参还是何种类型的量,在进行函数调用时,他们都必须具有确定的值,以便把这些值传递给形参,因此应预先用赋值,输入等办法使实参获取确定值,会产生一个临时变量
  3. 实参和形参在数量,类型,顺序应严格一致,否则会发生"类型不匹配"的错误
  4. 函数调用中发生的数据传送是单向的。即只能把实参的值传递给形参,而不能把形参的值反向传送给实参,因此在函数调用过程中,形参的值发生改变,而实参中的值不会改变
值传递,指针传递,引用传递的区别和效率
  1. 值传递,有一个形参向函数所属的栈拷贝数据的过程,如果传递的对象是类对象,或者大的结构体对象,将耗费一定的时间和空间。
  2. 指针传递:同样有一个形参向函数所属的栈拷贝数的过程,但拷贝的是一个固定4字节的地址。
  3. 引用传递:同样有上述的数据拷贝过程,但其是针对地址的,相当于为该数据所在的地址起了一个别名。
  4. 从效率上将,指针传递和引用传递比值传递效率更高。
静态变量什么时候初始化
  1. 初始化只有一次,但可以多次赋值,在主程序之前,编译器已经为其分配好了内存
  2. 静态变量和全局变量一样,数据都存放在全局区域,所以主程序之前,编译器已经为其分配好内存,但在C和C++中静态局部变量的初始化节点又优点不一样,在C中,初始化发生在代码执行之前,编译阶段分配好内存之后,就会进行初始化,所以我们看到在C语言中无法使用变量对静态局部变量进行初始化,在程序运行结束后,变量所处的全局内存会被全部回收
  3. 在C++中,初始化在执行相关代码是才会进行初始化,主要是由于C++引入对象后,要进行初始化必须执行相应构造函数和析构函数,在构造函数或析构函数中经常会需要进行某些程序中需要进行的特定操作,并非简单的分配内存。所以C++标准定为全局或静态对象是有首次用时才会进行构造,并通过atexit()来管理。在程序结束,按照构造顺序反方向进行逐个析构,所以在C++中是可以使用变量对静态局部变量进行初始化
consnt关键字的作用
  1. 阻止一个变量被改变,可以使用const关键字,在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它
  2. 对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或者二者同时为const
  3. 在一个函数声明中,const可以修饰形参,表示它是一个输入输出参数,在函数内部不能改变其值
  4. 对于类的成员函数,若指定其为const类型,则表示其是一个常函数,不能修改类的成员变量,类的常对象只能访问类的常成员函数
  5. 对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为"左值"
  6. const成员函数可以访问非const对象的非const数据成员,const数据成员,也可以访问const对象内的所有数据成员
  7. 非const成员函数可以访问非const对象的非const数据成员,const数据成员,但不可以访问const对象的任意数据成员
  8. 一个没有明确声明为const的成员函数被看作是将要修改对象中数据成员的函数,而且编译器不允许它为一个const对象所调用,因此const对象只能调用const成员函数
  9. const类型变量可以通过类型转换符const_cast将const类型转换为非const类型
  10. const类型变量必须定义的时候进行初始化,因此也导致如果类的成员变量中有const类型的变量,那么该变量必须在类的初始化列表中进行初始化
什么是类的继承
  1. 类与类之间的关系

    • has-A包含关系,用以描述一个类有多个部件类构成,实现has-A关系用类的成员属性表示,即一个类的成员属性是另一个已经定义好的类
    • use-A一个类使用另一个类,通过类之间的成员函数相互联系,定义友元或者通过传递参数的方式来实现
    • is-A继承关系,关系具有传递性
  2. 继承的相关概念

    所谓的继承就是一个类继承了另一个类的属性和方法,这个新类包含了上一个类的属性和方法,被成为子类或者派生类,被继承的类称为父类或者基类

  3. 继承的特点

    子类拥有父类的所有方法与属性,子类可以拥有父类没有的属性和方法,,子类对象可以当作父类对象使用

  4. 继承中的访问控制

    public,protected,private

  5. 继承中的构造和析构函数

  6. 继承中的兼容性原则

什么是内存泄漏,如何检测

内存泄漏

一般我们常说的内存泄漏是指堆内存泄漏。堆内存是值程序从堆中分配的,大小任意的内存块,使用完后必须显式释放的内存。应用程序使用malloc,realloc,new等函数从堆中分配到块内存,使用完之后,程序必须负责型用的调用free或者delete释放该内存块,否则,这块内存就不能继续使用了,我们就说这块内存泄露了。

避免内存泄漏的几种方式

  • 计数法:使用new或者malloc时,让该数+1,delete或者free时,该数-1,程序执行完,打印这个数,如果不为0则表示存在内存泄漏
  • 一定要将基类的析构函数声明为虚函数
  • 对象数组的释放一定要用delete[]
  • 有new就有delete,有malloc就有free,保证他们成对出现

检测工具

  • linux下可以使用Vargrind工具
  • Windows下可以使用CRT库
面向对象的三大特性

三大特性:封装,继承和多态

  1. 继承

    让某种类型对象获取另一个对象的属性和方法

    它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展

    常见的继承有三种

    1. 实现继承:指使用基类的属性和方法而无需额外编码的能力
    2. 接口继承:指仅使用属性的方法和名称,但是子类必须提供实现的能力
    3. 可视继承:指子类使用基类外观和实现代码的能力(C++中并不怎么用)

    例如:将人定义为一个抽象类,拥有姓名,性别,年龄等公共属性,吃饭,睡觉,走路等公有方法,在定义一个具体的人时,就可以继承这个抽象类,既保留了公共属性和方法,也可以在此基础上扩展跳舞,唱歌等特有方法。

  2. 封装

    数据和代码捆绑在一起,避免外界干扰和不确定性访问

    封装,也就是把客观事物封装成抽象类,并且类可以把自己的数据和方法只让可信的类或者对象进行操作,对不可信的进行隐藏,例如把公有数据或方法使用public修饰,而不希望外界访问的数据或方法采用private修饰。

  3. 多态

    同一事物表现出不同事物的能力,即向不同对象发送同一消息,不同对象在接受时会产生不同的行为重载实现编译时多态,虚函数实现运行时多态

    多态性是允许你将父对象设置为和一个或更多的他的子对象的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同方式运作。简单一句话:允许将子对象类型的指针赋值给符类类型的指针

    实现多态有两种方式:覆盖(重写)和重载

    覆盖(重写):是指子类重新定义父类的虚函数的做法

    重载:是指允许存在多个同名函数,而这些函数的参数表不同。

    例如:基类是一个抽象对象–人,那教师,运动员也是人,而使用这个抽象对象既可以表示教师,也可以表示运动员。

成员初始化列表的概念,为什么用它会快一些

成员初始化列表的概念

在类的构造函数中,不在函数体内部对成员赋值,而是在构造函数的花括号前面使用冒号和初始化列表赋值

效率

用初始化列表会更快一些的原因是,对于类型,它少了一次调用构造函数的过程,而在函数体中赋值则会多一次调用,而对于内置数据类型则没有差别。

#include <iostream>
using namespace std;
class A
{
public:
    A()
    {
        cout << "默认构造函数A()" << endl;
    }
    A(int a)
    {
        value = a;
        cout << "A(int "<<value<<")" << endl;
    }
    A(const A& a)
    {
        value = a.value;
        cout << "拷贝构造函数A(A& a):  "<<value << endl;
    }
    int value;
};

class B
{
public:
    B() : a(1)
    {
        b = A(2);
    }
    A a;
    A b;
};
int main()
{
    B b;
}

//输出结果:
//A(int 1)
//默认构造函数A()
//A(int 2)

从代码运行结果可以看出,在构造函数体内部初始化的对象b多了一次构造函数的调用过程,而对象a没有,由于对象成员变量的初始化动作发生在进入构造函数之前,对内置类型没有什么影响,但如果有些成员是类,那么在进入构造函数之前,会先调用一次默认构造函数,进入构造函数之后所做的事其实就是一次赋值操作,所以如果是在构造函数内进行赋值操作的话,等于是一次默认构造加一次赋值,而初始化列表只做一次赋值操作。

C++函数调用的压栈过程
  1. 从代码理解

    #include <iostream>
    using namespace std;
    
    int f(int n) 
    {
    	cout << n << endl;
    	return n;
    }
    
    void func(int param1, int param2)
    {
    	int var1 = param1;
    	int var2 = param2;
    	printf("var1=%d,var2=%d", f(var1), f(var2));//如果将printf换为cout进行输出,输出结果则刚好相反
    }
    
    int main(int argc, char* argv[])
    {
    	func(1, 2);
    	return 0;
    }
    //输出结果
    //2
    //1
    //var1=1,var2=2
    

    当函数从入口函数main函数开始执行时,编译器会将我们操作系统的运行状态,main函数的返回地址,main的参数,main函数中的变量,进行依此压栈。

    当main函数开始调用func()函数时,编译器此时会将main函数运行状态进行压栈,再将func()函数的返回地址,func()函数的参数从左往右,func()定义变量依此压栈。

    当func()函数开始调用f()函数时,编译器此时会将func()函数的运行状态进行压栈,再将函数的返回地址,f()函数的参数从左往右,func()定义的变量依此压栈。

    从输出结果看,函数f(var1),f(var2),依此入栈,而后先执行f(var2),再执行f(var1),最后打印整个字符串,将栈中的变量依此弹出,最后主函数退出。

  2. 文字化描述

    函数的调用过程:

    1. 从栈空间分配存储空间
    2. 从实参的存储空间复制值到形参栈空间
    3. 进行运算

    形参在函数未调用之前都是没有分配存储空间的,在函数调用结束后,形参弹出栈空间,清除形参空间。

    数组作为参数的函数调用方式是地址传递,形参和实参都指向相同的空间,调用完成后,形参指针被销毁,但所指的空间依然存在,不饿能也不会被销毁。

    当函数有多个返回值时,不能用普通的return的方式实现,需要通过传回地址的形式进行,即地址/指针传递。

移动构造函数
  1. 我们用对象a初始化对象b,后对象a我们不在使用了,但是对象a的空间还在(在析构之前),既然拷贝构造函数实际上就是把a对象的内同复制一份到b中,那为什么我们不能直接时候a的空间呢?这样就避免了新的空间分配,大大降低了构造的成本,这就是移动构造函数设计的初衷。

  2. 拷贝构造函数中,对于指针,我们一定采取深拷贝,而移动构造函数中,对于指针,我们使用浅拷贝。浅拷贝之所以危险,是因为两个指针共同指向一片内存空间,若第一个指针释放,则另一个指针的指向就不合法了

    所以我们只要避免第一个指针释放空间就可以了。避免的方法就就是将第一个指针置为NULL,这样在调用析构函数的时候,由于有判断是否为NULL的语句,所以析构a的时候并不会回收a的空间。

  3. 移动构造函数参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但移动构造函数的初值时一个右值引用,意味这,移动构造函数的参数是一个右值或者亡值得引用,也就是说,只使用一个右值,或者亡值初始化另一个对象得时候,才会使用移动构造函数。而那个move语句,就是将一个左值变成一个亡值。

C++中将临时变量作为返回值时得处理过程

首先明白一件事,临时变量,在函数调用中是被压到程序进程的栈中的,当函数退出时,临时变量出栈,即临时变量已经被销毁了。临时变量占用的内存空间没有被清空,但是可以被分配给其他变量,所以有可能在函数退出时,该内存已经被修改了,对于临时变量来说是没有意义的值了。

C语言规定,16bit程序中,返回值保存在ax寄存器中,32bit程序中,返回值保持在eax寄存器中,如果是64bit程序,edx寄存器保存高32bit,eax寄存器保存低32bit。

由此可见,函数调用结束后,返回值被临时存储到寄存器中,并没有放到堆或栈中,也就是说与内存没有关系了,当退出函数的时候,临时变量可能被销毁,但是返回值却被放到寄存器中与临时变量的生命周期没有关系。

如果我们需要返回值,一般使用赋值语句就可以了。

静态类型和动态类型,静态绑定和动态绑定
  • 静态类型:对象在声明时采用的类型,在编译期既已确定
  • 动态类型:通常指一个指针或引用目前所指对象的类型,是在运行期决定的
  • 静态绑定:绑定的是静态类型,所对应的函数或者属性依赖于对象的静态类型,发生在编译期
  • 动态绑定:绑定的是动态类型,所对应的函数或者属性依赖于对象的动态类型,发生在运行期

从定义可看出,非虚函数一般是静态绑定,而虚函数是动态绑定(如此才可实现多态)。

 #include <iostream>
using namespace std;

class A
{
public:
	/*virtual*/ void func() { std::cout << "A::func()\n"; }
};
class B : public A
{
public:
	void func() { std::cout << "B::func()\n"; }
};
class C : public A
{
public:
	void func() { std::cout << "C::func()\n"; }
};
int main()
{
	C* pc = new C(); //pc的静态类型是它声明的类型C*,动态类型也是C*;
	B* pb = new B(); //pb的静态类型和动态类型也都是B*;
	A* pa = pc;      //pa的静态类型是它声明的类型A*,动态类型是pa所指向的对象pc的类型C*;
	pa = pb;         //pa的动态类型可以更改,现在它的动态类型是B*,但其静态类型仍是声明时候的A*;
	C *pnull = NULL; //pnull的静态类型是它声明的类型C*,没有动态类型,因为它指向了NULL;
    
    pa->func();      //A::func() pa的静态类型永远都是A*,不管其指向的是哪个子类,都是直接调用A::func();
	pc->func();      //C::func() pc的动、静态类型都是C*,因此调用C::func();
	pnull->func();   //C::func() 不用奇怪为什么空指针也可以调用函数,因为这在编译期就确定了,和指针空不空没关系;
	return 0;
}
 

如果将A类中的virtual注释去掉,则运行结果是:

pa->func();      //B::func() 因为有了virtual虚函数特性,pa的动态类型指向B*,因此先在B中查找,找到后直接调用;
pc->func();      //C::func() pc的动、静态类型都是C*,因此也是先在C中查找;
pnull->func();   //空指针异常,因为是func是virtual函数,因此对func的调用只能等到运行期才能确定,然后才发现pnull是空指针;

在上面的例子中:

  • 如果基类A中的func不是virtual函数,那不论pa,pb,pc指向哪一个子类,早在编译期确定了。
  • 同样的空指针也能直接调用no-virtual函数而不报错(这也说明一定要做空指针检查!),因此静态绑定不能实现多态
  • 如果func是虚函数,那所有的调用都要等到运行时根据其指向的对象才能确定,比起静态绑定自然是要有性能损失的,但却能实现多态特性

总结:

  • ​ 静态绑定发生在编译期,动态绑定发生在运行期
  • 兑现的动态类型可以更改,但静态类型无法更改
  • 要想实现多态,必须使用动态绑定
  • 在继承体系中只有虚函数使用的是动态绑定,其他全部是静态绑定
引用是否能够实现动态绑定,为什么?

可以实现

引用在创建时必须初始化,在访问虚函数时,编译器会根据其所绑定的对象类型决定要调用哪个函数,注意只能调用虚函数。

#include <iostream>
using namespace std;

class Base 
{
public:
	virtual void  fun()
	{
		cout << "base :: fun()" << endl;
	}
};

class Son : public Base
{
public:
	virtual void  fun()
	{
		cout << "son :: fun()" << endl;
	}
	void func()
	{
		cout << "son :: not virtual function" <<endl;
	}
};

int main()
{
	Son s;
	Base& b = s; // 基类类型引用绑定已经存在的Son对象,引用必须初始化
	s.fun(); //son::fun()
	b.fun(); //son :: fun()
	return 0;
}

需要说明的是虚函数才具有动态绑定,在上面代码中,Son类中还有一个非虚函数func(),这在b对象中是无法调用的,如果使用基类指针来指向子类也是一样的。

全局变量和局部变量有什么区别?
  1. 声明周期不同:全局变量随主程序创建而创建,随主程序销毁而销毁;局部变量在局部函数内部,甚至局部循环体等内部存在,退出就不存在;
  2. 使用方式不同:通过声明后全局变量在程序的各个部分都使用到,局部变量分配在堆栈区,只能在局部使用
  3. 操作系统和编译器通过内存分配的位置可以区分二者,全局变量分配在全局数据段并且在程序开始运行的时候被加载。局部变量则被分配在堆栈里面。
判断两个浮点数是否相等

对两个浮点数判断大小是否相等时不能直接使用==来判断,会出错,对两个浮点数比较只能通过相减并与预先设定的精度比较,记得要取绝对值,浮点数与0的比较也应该注意。

C++中的指针参数传参和引用参数传参有什么区别?底层原理是?
  1. 指针参数传递本质上是值传递,它传递的是一个地址

    值传递过程中,被调函数的形参作为被调函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的实参值,从而形成实参的一个副本。

    值传递的特点是,被调函数对形参的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值(形参指针变了,实参指针不会变)。

  2. 引用参数传递过程中,被调函数的形参也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量地址。

    被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主函数中的实参变量。

    因此,被调函数对形参的任何操作都会影响主函数中的实参变量。

  3. 引用和指针的传递效率是不同的,虽然他们都是在被调函数空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址操作得到主调函数中的相关变量。

而对于指针传递的参数,如果改变被调函数中的指针地址,他将应用补刀主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量,那就得使用指向指针的指针或者指针引用。

  1. 从编译的角度来讲,程序在编译时分别将指针和引用添加到符号表上,符号表中记录的时变量名以及所对应地址

指针变量在符号表上对应的地址为指针变量的地址,而引用在符号表上对应的地址值为引用变量的地址值(与实参名字不同,地址相同)

符号表生成后就不会在更改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。

例子:

​ 定义一个结构体

struct ST
{
public:
	int num;
	string name;
	ST(int num, string name)
	{
		this->num = num;
		this->name = name;
	}
    void putInfo()
	{
		cout << "学号:" << num << endl;
		cout << "姓名:" << name << endl;
	}
};

指针传递:

void dealProcess(ST* st)
{
	ST *newST =new ST(320, "小明");
	st = newST;//为临时指针变量赋予了新值,已和输入脱离了联系。
}

int main()
{
	int num = 1;
	ST st1(310, "小红");
	dealProcess(&st1);
	st1.putInfo();
	system("pause");
	return 0;
//输出结果
//学号:310
//姓名:小红
}

引用传递:

void dealProcess(ST& st)
{
	ST newST(320, "小明");
	st = newST;    
	&st=&newsT;//编译错误,不允许改变输入的地址
}
int main()
{
	int num = 1;
	ST st1(310, "小红");
	dealProcess(st1);
	st1.putInfo();
	system("pause");
	return 0;
//输出结果
//学号:320
//姓名:小明
}
类如何实现只能静态分配和动态分配
  1. 前者是把new/delete运算符重载为private属性;后者是把构造,析构函数设置为private属性,再用子类来动态创建
  2. 建立类的对象由两种方式:
    1. 静态建立,静态建立一个类对象,就是由编译器为对象在栈空间中分配内存
    2. 动态建立,A* p=new A();动态建立一个类对象,就是使用new运算符为对象在堆空间中分配内存。这个过程分为两步,第一步执行operator new()函数,在堆中搜索一块内存并进行分配,第二部调用类构造函数构造对象。
  3. 只有使用new运算符,对象才会被建立在堆上,因此只要限制new运算符就可以实现类对象只能建立在栈上,可以将new运算符设置为私有
C++中的继承和组合

组合也就是设计类的时候把要组合的类的对象加入该类中作为自己的成员变量。

组合的优点:

  1. 当前对象只能通过包含的那个对象去调用其方法,所以包含的对象的内部细节对当前对象时不可见。
  2. 当前对象与包含的对象是一个低耦合关系,如果修改包含对象的类中代码不需要修改当前对象类的代码
  3. 当前对象可以在运行时动态绑定所包含的对象,可以通过set方法给所包含对象赋值。

组合的缺点

  1. 容易产生过多的对象
  2. 为了能组合多个对象,必须仔细对接口进行定义
函数指针
  1. 什么是函数指针

    函数指针指向的时特殊的数据类型,函数的类型是由其返回的数据类型和其参数列表共同决定的,而函数的名称则不是其类型的一部分。

    一个具体函数的名字,如果后面不跟调用符号(即括号),则该名字就是函数的指针。

  2. 函数指针的声明方法

    int(*pf)(const int&,const int&);(1)

    上面的pf就是一个函数指针,指向所有返回类型为int,并且带有两个const int&参数的函数,注意*pf两边的括号是必须的,否则上面的定义就变成了:

    int *pf(const int&,const int&);(2)

    而这声明了一个函数pf,其返回值为int*,带有两个const int&参数。

  3. 为什么要有函数指针

    函数与数据相似,函数也有地址,我们希望在同一个函数中通过使用相同的形参在不同的事件产生不同的效果。

  4. 一个函数名就是一个指针,它指向函数的代码

    一个函数地址是该函数的进入点,也就是调用函数的地址。函数的调用可以通过允许函数名,也可以通过指向函数的指针来调用。函数指针还允许将函数作为变元传递给其他函数。

  5. 两种方法赋值:

    指针名=函数名;指针名=&函数名

内存对齐以及原因
  1. 分配内存的顺序是按照声明的顺序
  2. 每个变量相对于起始位置的偏移量必须是该变量类型大小的整数倍,不是整数倍空出内存,直到偏移量是整数倍为止
  3. 最后整个结构体的大小必须是里面变量类型最大值的整数倍

添加了#pragma pack(n)后规则就变成下面这样

  1. 偏移量要是n和当前变量大小中较小值的整数倍
  2. 整体大小要是n和最大变量大小中较小值的整数倍
  3. n值必须为1,2,4,8…,为其他值时就按照默认的分配规则
cout和printf有什么区别?

cout<<是一个函数,cout<<后可以跟不同的类型是因为cout<<已经存在针对各种数据的重载,所以会自动识别数据的类型

输出过程会首先将输出字符放入缓冲区,然后输出到屏幕

cout是由缓冲输出的

cout<<"abc"<<endl;
或者cout<<"abc\n";cout<<flush;这两个才是一样的

flush立即强迫缓冲输出,不是无缓冲输出

运算符重载
  1. 我们只能重载已有的运算符,而无权发明新的运算符;对于一个重载的运算符,其优先级和结合律与内置类型一致才可以,不能改变运算符操作个数。

  2. 两种重载方式:成员运算符和非成员运算符

    成员运算符比非成员运算符少一个参数

    下标运算符,箭头运算符必须是成员运算符

  3. 引入重载运算符是为了实现类的多态性。

  4. 当重载的运算符是成员运算符时,this绑定到左侧运算符对象。成员运算符的参数数量比运算符对象的数量少一个;至少含有一个类类型的参数。

  5. 从参数的个数推断到底定义的是哪种运算符,当运算符即使一元运算符又是二元运算符(+,-,*,&)

定义和声明的区别

如果是指变量的声明和定义:从编译原理上来说,声明是仅仅告诉编译器,有个某类型的变量会被使用,但是编译器并不会为它分配任何内存,而定义就是为了分配内存。

如果是指函数的声明和定义:声明一般在头文件里。对编译器说:这里有一个函数叫function(),让编译器知道这个函数的存在,定义一般放在源文件中,具体就是函数的实现过程,写明函数体。

全局变量和static变量的区别
  1. 全局变量的说明之前在冠以static就构成了静态全局变量

    全局变量本身就是静态存储方式,静态全局变量当然也是静态存储方式。

    这两种存储方式上并无不同,区别在于非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。

    而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其他源文件中不能使用它。由于静态全局变量的作用域限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其他源文件中引起的错误

    static全局变量与普通的全局变量的区别是static全局变量只初始化一次,防止在其他文件单元被引用。

  2. static函数和普通函数的区别?

    static函数与普通函数作用域不同。只在当前源文件中使用的函数应该说明为内部函数,内部函数应在当前文件中说明和定义。

    对于可在当前源文件以外使用的函数应该在一个头文件中说明,要使这些函数的源文件要包含这个头文件。static函数与普通函数最主要的区别是static函数在内存中只有一份,普通静态函数在每个被调用中维持一份拷贝的局部变量存在于堆栈中,全局变量存在于静态区中,动态申请数据存在于堆。

静态成员与普通成员的区别
  1. 声明周期

    静态成员变量从类被加载开始到类被卸载,一直存在。

    普通成员变量只有在类创建对象后才开始存在,对象结束,它的声明周期结束。

  2. 共享方式

    静态成员变量是全类共享;普通成员变量是每个对象单独享用的。

  3. 定义位置

    普通成员变量存储在栈或者堆中,而静态成员变量存储在静态全局区

  4. 初始位置

    普通成员在类中初始化;静态成员变量在类外初始化。

  5. 默认实参

    可以使用静态成员变量作为默认实参。

strcpy和memcpy区别
  1. 复制的内容不同。

    strcpy只能复制字符串

    memcpy可以复制任意内容,例如数组,结构体,类等

  2. 复制的方法不同

    strcpy不需要指定长度,遇到"\0"才结束,所以很容易溢出;memcpy则是根据其第三个参数决定复制的长度。

  3. 用途不同

    通常在复制字符串时用strcpy,而需要复制其他类型的数据时则一般使用memcpy。

strcpy,sprintf与memcpy这三个函数的不同
  1. 操作对象不同

    1. strcpy的两个操作对象均为字符串
    2. sprintf的操作对象可以是多种数据类型,目的操作对象时字符串
    3. memcpy的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。
  2. 执行效率不同

    memcpy最高,strcpy次之,sprintf的效率最低

  3. 实现功能不同

    1. strcpy主要实现字符串变量的拷贝
    2. sprintf主要实现其他数据类型格式到字符串的转化
    3. memcpy主要是内存块间的拷贝
volatile关键字的作用

volatile关键字时一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素修改,比如:操作系统,硬件或者其它线程等,遇到这个关键字声明的变量,编译器对访问该变量的代码就不在优化,从而可任意提供特殊地址的稳定访问。


  1. 讲讲用到的锁

  2. 项目中session的管理

  3. 编译链接的过程

  4. STL中vector如何扩容
    ,因此可以避免在其他源文件中引起的错误

    static全局变量与普通的全局变量的区别是static全局变量只初始化一次,防止在其他文件单元被引用。

  5. static函数和普通函数的区别?

    static函数与普通函数作用域不同。只在当前源文件中使用的函数应该说明为内部函数,内部函数应在当前文件中说明和定义。

    对于可在当前源文件以外使用的函数应该在一个头文件中说明,要使这些函数的源文件要包含这个头文件。static函数与普通函数最主要的区别是static函数在内存中只有一份,普通静态函数在每个被调用中维持一份拷贝的局部变量存在于堆栈中,全局变量存在于静态区中,动态申请数据存在于堆。

静态成员与普通成员的区别
  1. 声明周期

    静态成员变量从类被加载开始到类被卸载,一直存在。

    普通成员变量只有在类创建对象后才开始存在,对象结束,它的声明周期结束。

  2. 共享方式

    静态成员变量是全类共享;普通成员变量是每个对象单独享用的。

  3. 定义位置

    普通成员变量存储在栈或者堆中,而静态成员变量存储在静态全局区

  4. 初始位置

    普通成员在类中初始化;静态成员变量在类外初始化。

  5. 默认实参

    可以使用静态成员变量作为默认实参。

strcpy和memcpy区别
  1. 复制的内容不同。

    strcpy只能复制字符串

    memcpy可以复制任意内容,例如数组,结构体,类等

  2. 复制的方法不同

    strcpy不需要指定长度,遇到"\0"才结束,所以很容易溢出;memcpy则是根据其第三个参数决定复制的长度。

  3. 用途不同

    通常在复制字符串时用strcpy,而需要复制其他类型的数据时则一般使用memcpy。

strcpy,sprintf与memcpy这三个函数的不同
  1. 操作对象不同

    1. strcpy的两个操作对象均为字符串
    2. sprintf的操作对象可以是多种数据类型,目的操作对象时字符串
    3. memcpy的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。
  2. 执行效率不同

    memcpy最高,strcpy次之,sprintf的效率最低

  3. 实现功能不同

    1. strcpy主要实现字符串变量的拷贝
    2. sprintf主要实现其他数据类型格式到字符串的转化
    3. memcpy主要是内存块间的拷贝
volatile关键字的作用

volatile关键字时一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素修改,比如:操作系统,硬件或者其它线程等,遇到这个关键字声明的变量,编译器对访问该变量的代码就不在优化,从而可任意提供特殊地址的稳定访问。


  1. 讲讲用到的锁
  2. 项目中session的管理
  3. 编译链接的过程
  4. STL中vector如何扩容
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Exile_001

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

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

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

打赏作者

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

抵扣说明:

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

余额充值