C++常考知识点

C++知识点

关键字和库函数

字符串

三种方法保存字符串的方法:

  • char *s1:s1是一个指向字符串的指针;
  • char s2[]:s2是一个字符数组;
  • string s3:s3是一个string类的对象;

字符串常量:

"helloworld"为一个字符串常量,其保存在常量区,只读,程序结束后由系统释放空间,类型为const char *;

char * a=”string1”已不再支持,因为const char*char *类型不兼容;

概念辨析

  • char str[]="hello"

    • 等价于char str[]={'h', 'e', 'l', 'l', 'u','\0'};
    • str为数组名,也是数组首地址,*str为h,*(str+5)'\0'
    • str虽然是地址,但其并不是指针变量,typeid(str).name()char[8]
    • 类型char[8],char*,const char*均被printf("%s\n",str);打印
  • string str="hello"

    • 用字符串常量,拷贝构造了str对象,char[8],char*,const char*均可用于赋值或拷贝构造str对象

    • str.c_str()str.data()方法返回const char*类型的字符串

    • 转换为char *

        char *data;
      int len = s1.length();
      data = (char *)malloc((len + 1)*sizeof(char));
      s1.copy(data, len, 0);
      data[len] = '\0';
      
    • 转换为char[8]

      char buf[10];
      strcpy(buf, str.c_str());
      buf[str.length()] = '\0';
      

内存模型

char *s1 = “hello”;
char s2[] = “hello”;
内存模型如下
±----+ ±–±--±–±--±–±--+
s1: | *======> | h | e | l | l | o |\0 |
±----+ ±–±--±–±--±–±--+
±–±--±–±--±–±--+
s2: | h | e | l | l | o |\0 |
±–±--±–±--±–±--+

const

  1. const修饰变量/指针变量

    • 将其为const常量,使其值不能被改变,相比define还可以进行类型检查;

    • const int *p;int const *p;const修饰 *p, *p不能被改变,即指针指向的值不能改变;

    • int *const p;const修饰指针变量p,即p的值不能被改变,指针的指向不能改变;

  2. const修饰函数返回值,使得函数调用表达式不能作为左值。

  3. const修饰函数形参,使传递过来的参数只有只读权限,如传引用并添加const修饰;

  4. const修饰成员变量,成员变量只能在类内声明、定义,在构造函数初始化列表中初始化;

  5. const修饰成员函数,写在函数后面int show() const{},使得该成员函数对成员变量只有只读权限,相当于修饰const this*;

const与define的区别
  1. define 是在编译预处理阶段进行替换,const 是在编译阶段确定其值。
  2. const可以让编译器进行类型检查
  3. const 定义的常量占用静态存储区的空间,程序运行过程中只有一份。define在多处替换
  4. const 定义的常量可以进行调试。

static

  1. 修饰全局变量,或函数,使其仅在当前.cpp文件内有效,防止命名空间的污染
  2. 修饰局部变量,使其生命周期延长到程序结束。循环中static局部变量只会被声明和初始化一次。
  3. 修饰成员变量或成员函数,将其声明为属于该类的公有资源
    • 静态成员函数不能声明成虚函数(virtual)、const 函数和 volatile 函数。上述三个修饰的机制依赖对象的实例化,而static修饰类域公有资源,语义矛盾
静态成员变量与静态成员函数
  1. 静态成员变量的作用域是属于整个类家族的,包括声明该静态成员变量的类,以及继承了这个类的类;

  2. 静态成员变量之只能类内声明,类外初始化;即声明和初始化不得不分开,无法在初始化列表对其初始化,但可在函数体内赋值;

  3. 静态成员不通过对象访问,可通过指定类作用域直接访问。当然也可以通过成员函数或静态成员函数访问;

  4. 私有静态成员由于权限问题,也无法指定类作用域直接访问,只能通过静态成员函数或成员函数访问;

    class A{
    private:
    	static int my_svalA;
    public:
        static int svalA;
        void changeA(int value){my_svalA=value;}
        static void schangeA(int value){my_svalA=value;}
    };
    int A::my_svalA=4396;	//如不初始化,动态检查不到,但一旦编译时构造A类(A a或者遇到A::)则会报错
    int A::svalA=4396;
    int main(){
        A::svalA=666;		
        A::schangeA(777);
        A a;
        a.changeA(888);
        a.schangeA(123);//编译器会警告,相当于A::schangeA(123);
    }
    

extern

  1. 置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。
  2. extern “C"{}:连接指定,是用C语言的编译规则。
  3. externstatic修饰全局变量或函数时具有相反的作用,是水火不容的。
  4. extern 声明的变量,要与其在另一个.cpp中声明的格式完全相同。如char a[]不能对应 extern char *a;具有较强耦合性,当源定义修改,extern声明没有改变,则可能引发错误。因此一般使用#include头文件,而不使用extern

inline

  1. 修饰函数:使该函数在编译阶段展开为代码附加在程序段中,而不使用普通函数调用的跳转机制。
  2. 函数体积较小可以增加效率,一般函数体只有几行的时候使用。
  3. 类内定义的成员函数默认为inline函数,类外定义的函数则需要使用inline显式声明。

运算符重载

设重载的运算符为$

  1. operator$重载为全局函数operator$(a,b),那么a$b相当于operator$(a,b);
  2. operator$重载为成员函数operator$(b),那么a$b相当于a.operator$(b);
  3. 返回值为引用类型时,才能实现链式编程;
  4. 左移或右移运算符不得不重载为全局函数,因为我们希望使用cout<<num,而非num<<cout;operator<<的两个参数:p代表传入对象,获取需要输出的值;cout代表向终端输出流; 如果作为成员函数,只能p.operator<<(cout),所以只能通过p<<cout调用只有重载为全局函数,且形参列表中ostream&在前,才能实现cout<<p的形式形参必须是ostream&,因为cout无法被拷贝,返回值实际上可以为其他,但返回ostream&,才能实现连用;友元声明为private或public没有区别。
  5. operator+可重载为成员函数或全局函数,认为规定其重载为成员函数
  6. 前置++可重载为成员函数或全局函数,若不返回Complex&,则++++num只会递增一次。
  7. 后置++可重载为成员函数或全局函数,使用const修饰返回值,使函数返回的对象只能作为左值,即num++++会编译报错;后置++的逻辑是对象自增,但返回自增前的值,通过构造一个临时对象实现。这就决定num++++只能自增一次,其后续的++自增的是临时对象,没有意义。后置++不能返回引用,返回临时对象的引用是错误的。
  8. 赋值运算符必须返回引用才能使链式编程有效,若不返会引用a=b=c报错,传入了一个临时对象的引用,(a=b)=c不报错,但a和b不会被c复制,c的值赋给了临时对象。
  9. 对函数调用运算符()的重载也叫仿函数。
#include<bits/stdc++.h>
using namespace std;
class Complex{
    int a,b;
public:
    Complex():a(0),b(0){};
    Complex(int _a,int _b):a(_a),b(_b){};

    friend ostream& operator<<(ostream& out, Complex& p);
    friend ostream& operator<<(ostream& out, const Complex& p);
    Complex operator+(const Complex &p2)const;
    Complex& operator++();
    const Complex operator++(int);
    Complex& operator=(const Complex &p2);
};
Complex Complex:: operator+(const Complex &p2)const{
    return {a+p2.a,b+p2.b};
}
ostream& operator<<(ostream& out, const Complex& p) {
    out << p.a << "+" << p.b<<'i';
    return out;
}
ostream& operator<<(ostream& out, Complex& p) {
    out << p.a << "+" << p.b<<'i';
    return out;
}
Complex& Complex:: operator++(){
    a++;
    return *this;
}
const Complex Complex::operator++(int){			//后置++写法特殊,且一般采用调用前置++实现
    Complex temp=*this;
    ++*this;
    return temp;
}
Complex& Complex::operator=(const Complex &p)	//浅拷贝的=运算符重载是默认的,这里可以直接=default;
{
    a=p.a;b=p.b;
    return *this;
}

控制对象只能建立在栈区

//将new和delete运算符重载对A特化的,且设为私有,外部便无法使用了
class  A  {  
private :  
    void * operator  new ( size_t  t){}      // 注意函数的第一个参数和返回值都是固定的   
    void  operator  delete ( void * ptr){}  // 重载了new就需要重载delete   
public :  
    A(){}  
    ~A(){}  
}; 

多态

  1. 静态多态/编译期多态:

    编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用那个函数,如果有对应的函数就调用该函数,否则出现编译错误。

    • 函数重载:包括普通函数的重载、运算符重载和成员函数的重载,可以重用函数名,编译器根据函数参数类型推断调用的是具体哪个函数实现,并在编译阶段完成对函数地址的绑定。
    • 函数模板:将类型作为参数,传递给模板,可使编译器生成该类型的函数。
  2. 动态多态/运行时多态:

    在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。

    • 条件:1.具有继承关系;2.派生类重写基类的虚函数;3.父类的指针或引用指向派生类的对象;
  3. 为什么要使用多态?

    1.可以提高代码的复用性,应用程序不必为每一个派生类编写功能调用,只需要对抽象基类进行处理即可。

    2.可以解决项目中紧偶合的问题,提高程序的可扩展性。派生类的功能可以被基类的方法或引用变量所调用,可以向后兼容,可以提高可扩充性和可维护性。

虚表指针、虚函数表、虚函数

img
  1. 如题,三者构成多态机制。当类中有虚函数或者其继承了具有虚函数的类时(无论自己有没有写虚函数),都会触发多态机制,即编译器在内存的模块区域为该类创建一个虚函数表。在该类对象实例化时,会隐形的包含一个虚表指针,指针的值为虚函数表的地址。

  2. 虚表指针:一个对象一个虚表指针,构造时初始化为虚表的地址。

    VS所带编译器是把虚表指针放在了内存的开始处(0地址偏移),然后再是成员变量;在构造函数创建虚表指针以及虚表的。

  3. 虚函数表:一个类具有一个虚函数表,里面存放着一些虚函数的地址。子类虚函数表中的val(函数地址),会继承(copy)父类表中的值,除非子类重写父类的虚函数,相当于将表中的这条数据覆盖为新的。

    虚函数表存放在全局区。

  4. 虚函数:虚函数与的实现存放在另一处,可被虚函数表索引到。一个虚函数可能被多个类检索到。

  5. 基类的指针或引用指向派生类的对象:A *ptr=new B();首先一个ptr指向B型对象b,那么ptr的值即对齐这个b的基地址,即ptr作用的一块区域内是B结构的一套东西。这个ptr是A类型的,只是限制了其寻址范围为A结构的形式。如A中2个int,B中继承了这两个int并新增一个int,那么ptr只能访问前两个int。A结构和B结构的内存中都有虚表指针(凡是多态类肯定有啊,而且是最先初始化的),因此ptr必然能寻址到B中的虚表指针,进而访问B的虚表找到B的虚函数。

  6. 子类的虚表指针可以理解为是对父类虚表指针的继承,只不过其在初始化时指向了子类的虚函数表。子类中定义新的虚函数,并不会增加虚表指针的个数,只是扩充或覆盖子类的虚函数表

  7. 多重继承:一个类继承两个父类,那么其中会包含两套完整父类的东西。自然包括两个虚表指针和两个虚表。

  8. 多重虚继承:虚函数表不会生成多个,但虚表指针会继承多个。

内存相关

C++ 内存分区

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3dkVWBf6-1633963291844)(D:\Users\tpytpytpy\Desktop\C++常考概念.assets\70-16339623821363.png)]

#include <iostream>
using namespace std;

/*
说明:C++ 中不再区分初始化和未初始化的全局变量、静态变量的存储区,如果非要区分下述程序标注在了括号中
*/

int g_var = 0; // g_var 在全局区(.data 段)
char *gp_var;  // gp_var 在全局区(.bss 段)

int main()
{
    int var;                    // var 在栈区
    char *p_var;                // p_var 在栈区
    char arr[] = "abc";         // arr 为数组变量,存储在栈区;"abc"为字符串常量,存储在常量区
    const char *p_var1 = "123456";    // p_var1 在栈区;"123456"为字符串常量,存储在常量区
    static int s_var = 0;       // s_var 为静态变量,存在静态存储区(.data 段)
    p_var = (char *)malloc(10); // 分配得来的 10 个字节的区域在堆区
    free(p_var);
    return 0;
}

存放函数的局部变量、函数参数、返回地址等,由编译器自动分配和释放。

控制对象只能建立在栈区
//将new和delete运算符重载对A特化的,且设为私有,外部便无法使用了
class  A  {  
private :  
    void * operator  new ( size_t  t){}      // 注意函数的第一个参数和返回值都是固定的   
    void  operator  delete ( void * ptr){}  // 重载了new就需要重载delete   
public :  
    A(){}  
    ~A(){}  
}; 

动态申请的内存空间,就是由 malloc 分配的内存块,由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收。

控制对象只能建立在堆区
class  A{  
protected :  
    A(){}  	//只私有构造函数,无法使用new,因为operator ()分配内存时不能提供构造功能。
    ~A(){}  //只要析构函数被保护,在栈区创建对象时就会被编译器检测到,无法被自动析构的对象便不能创建在栈区。但为了良好的可读性,将构造函数也被保护,这样创建和销毁对象都使用自定义的函数接口,而不是一个new一个destroy。
public :  
    static  A* create()  		//只能通过提供的creat()接口获得堆区的对象
    {  return   new  A();  }  
    void  destory()  			//只能通过提供的destory()接口获得堆区的对象
    {  delete   this ;  }  
};  
new 和 malloc
  • malloc :成功申请到内存,返回指向该内存的指针;分配失败,返回 NULL 指针。
    new :内存分配成功,返回该对象类型的指针;分配失败,抛出 bac_alloc 异常。
  • malloc、free 是库函数,而new、delete 是关键字。
    new 申请空间时,无需指定分配空间的大小,编译器会根据类型自行计算;malloc 在申请空间时,需要确定所申请空间的大小。
  • 对于自定义的类型,new 首先调用 operator new() 函数申请空间(底层通过 malloc 实现),然后调用构造函数进行初始化,最后返回自定义类型的指针;delete 首先调用析构函数,然后调用 operator delete() 释放空间(底层通过 free 实现)。malloc、free 无法进行自定义类型的对象的构造和析构。
  • new 操作符从自由存储区上为对象动态分配内存,而 malloc 函数从堆上动态分配内存。(自由存储区不等于堆)
malloc 的原理
  • 当开辟的空间小于 128K 时,调用 brk() 函数,通过移动 _enddata 来实现;
  • 当开辟空间大于 128K 时,调用 mmap() 函数,通过在虚拟地址空间中开辟一块内存空间来实现。
  • brk() 函数实现原理:向高地址的方向移动指向数据段的高地址的指针 _enddata。
  • mmap 内存映射原理:进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域;调用内核空间的系统调用函数 mmap(),实现文件物理地址和进程虚拟地址的一一映射关系;进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝。

全局区/静态存储区(.bss 段和 .data 段)

存放全局变量和静态变量,程序运行结束操作系统自动释放,在 C 语言中,未初始化的放在 .bss 段中,初始化的放在 .data 段中,C++ 中不再区分了。

怎么在main函数执行之前执行一段代码
  1. C++程序先创建全局变量,然后在执行main函数
  2. 全局变量声明时初始化,并调用函数返回值int g_iValue=func();
  3. 全局变量是类类型时,会先执行类的构造函数,因此在main前,全局变量的构造函数内的代码会被先执行
  4. 类的静态成员在mian函数前初始化。饿汉式单例模式。

常量存储区(.data 段)

存放的是常量,不允许修改,程序运行结束自动释放。

字符串常量”hello"存放在常量区

代码区(.text 段)

存放代码,不允许修改,但可以执行。编译后的二进制文件存放在这里。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值