• C++基础知识


    C/C++基础知识

    数组和指针的区别

    • 概念不同. 指针相当于一个变量,它存放的是数据在内存中的地址;数组是用于储存多个相同类型数据的集合
    • 赋值不同. 同类型指针变量可以相互赋值,数组不行,只能一个一个元素的赋值或拷贝
    • 访问数据不同. 指针是间接访问数据,获取指针,先解引用,再访问指针指向的地址中的内容;数组是直接访问
    • sizeof意义不同.
      数组所占存储空间的内存:sizeof(数组名)
      数组的大小:sizeof(数组名)/sizeof(数据类型)
      在32位平台下,sizeof(指针名)是4,在64位平台下,sizeof(指针名)是8
    • 指针和数组名异同. 指针和数组名都可以表示地址,但指针是变量,可以修改;数组名是常量,不可修改赋值
    • 传参.
      数组传参时会退化成指针
    1. 退化的意义:C语言只会以值拷贝的方式传递参数,参数传递时,如果拷贝整个数组,效率会大大降低,并且在参数位于栈上,太大的数组拷贝将会导致栈溢出。
    2. 因此,C语言将数组的传参进行了退化。将整个数组拷贝一份传入函数时,将数组名看做常量指针,传数组首元素的地址。

    指针数组和数组指针

    指针数组本质是数组,数组指针本质是指针,谁优先级高,本质是谁

    • 指针数组:它实际上是一个数组,数组的每个元素存放的是一个指针类型的元素。
      int* arr[8];
    1. 优先级问题:[]的优先级比*高
    2. 说明arr是一个数组,而int*是数组里面的内容
    3. 这句话的意思就是:arr是一个含有 * 和 int* 的数组
    • 数组指针:它实际上是一个指针,该指针指向一个数组。
      int (*arr)[8];
    1. 由于[]的优先级比*高,因此在写数组指针的时候必须将*arr用括号括起来
    2. arr先和*结合,说明p是一个指针变量
    3. 这句话的意思就是:指针arr指向一个大小为8个整型的数组。

    字符数组和字符串常量

    char arr[]=“hello”; //字符数组
    char *arr2=“hello”; //字符串常量

    • char arr[]=“hello”,此处的赋值是将常量区的字符串"hello"拷贝到了堆栈区的数arr的空间了。数组arr是在堆栈区开辟了空间,此时是可以修改字符串的值,因为修改的是堆栈区的字符串的值。另外此时的数组名arr是堆栈区中的"hello"的首地址。
    • char *arr2=“hello”,指针arr2是存储在堆栈区,但字符串是常量,存储在常量区,只是指针arr指向了存储在常量区的字符串首地址,此时不能改变常量区的字符串的值。

    const char arr[]=“hello”; //这里hello本来是在栈上的,但是编译器可能会做某些优化,将其放到常量区
    const char *arr2=“hello”; //字符串hello保存在常量区,const本来是修饰arr2指向的值不能通过arr2去修改,但是字符串hello在常量区,本来就不能改变,所以加不加const效果都一样

    inline和宏的区别

    • inline是在编译期间展开,宏在预处理时替换
    • inline函数直接嵌入到目标代码内,宏只是简单的文本替换
    • inline函数会进行类型安全检查和参数有效性检查,而宏不会
    • inline函数是函数,宏不是
    • 宏定义时有可能产生二义性,而inline函数不会
    • inline可以不展开,只是对编译器的建议,而宏必须展开

    引用和指针的区别

    • 指针有内存分配,而引用只是一个别名. 引用声明时必须初始化,从而指向一个已经存在的对象
    • 引用可以看做指针常量,指针是一个存储地址的变量. 指针在运行时可以改变其所指向的值,而引用一旦和某个对象绑定后就不再改变。这句话可以理解为:指针可以被重新赋值以指向另一个不同的对象,但是引用则总是指向在初始化时被指定的对象,以后不能改变,但是指定的对象其内容可以改变。
    • 引用创建时必须初始化,且不为空,指针创建时可以为空. 不存在指向空值的引用这个事实,意味着使用引用的代码效率比使用指针的要高。因为在使用引用之前不需要测试它的合法性。相反,指针则应该总是被测试,防止其为空。
    • 指针和引用的自增运算符意义不同. 指针是对内存地址的自增,引用是对值的自增
    • sizeof的意义不同. 使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小
    • 没有引用常量,有指针常量. 没有int& const p,有int* const p.
      常量指针和常量引用是存在的,const int *p和cosnt int &p,都表示指向的对象为常量常量引用形参的好处:(1)使用引用作为形参,不会产生新的变量,减少形参和实参传递的开销;(2)使用引用可能会导致实参随着形参的改变而改变。声明为const之后就会消除这种副作用。
    • 参数传递. 作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引 用的修改都会改变引用所指向的对象
    • 多级指针,一级引用. 指针可以有多级指针(**p),而引用只有一级

    C++中class和struct的区别

    C中的strcut不能有函数,但C++中可以。C++中的struct对C中的struct进行了扩充,它已经不再只是一个包含不同数据类型的数据结构了,它已经获取了太多的功能。struct能包含成员函数吗? 能!struct能继承吗? 能!!struct能实现多态吗? 能!!!

    • 默认的继承访问权限. struct是public的,class是private的
      class B : public A就是为了指明是public继承,而不是用默认的private继承,若class B : A则是private继承
    • 定义模板参数. class这个关键字还用于定义模板参数,就像typename。但关键字struct不用于定义模板参数。

    new/delete和malloc/free区别

    malloc和calloc间的主要区别在于后者在返回指向内存的指针之前把它初始化为0。另一个区别是calloc的参数包括所需的元素的数量和每个元素的字节数

    • 属性不同. new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持c
    • 申请的内存所在位置. new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存.
    • 返回类型安全性. new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。
      而malloc内存分配成功则是返回void*类型,需要通过强制类型转换将空类型指针转换成我们需要的类型。
    • 内存分配失败时的返回值. new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。
    • 是否需要指定内存大小. 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的大小
      int *pi=new int;//在自由存储区中分配创建了一个整形对象,并返回一个指向该对象的地址来初始化指针pi。
      int *pi=new int();//对指针pi指向的地址的值进行了初始化为0
      int *pi=new int(1024);//初始化为1024。
      int *p=(int *)malloc(100);//指向整型的指针p指向一个大小为100字节的内存的地址
      int *p=(int )malloc(25sizeof(int));//指向整型的指针p指向一个25个int整型空间的地址
    • 数组分配内存(与前一个特点类似). new中分配内存对于变量和数组不同,malloc分配内存则相同
      int *pi=new int[]; //指针pi所指向的数组未初始化
      int *pi=new int[n]; //指针pi指向长度为n的数组,未初始化
      int *pi=new int; //指针pi所指向的地址初始化为0
    • 是否调用构造函数/析构函数. new会先调用operator_ new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator_ delete函数释放内存(通常底层使用free实现)。malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
    • 能否重载. opeartor_new/operator_delete允许重载,malloc/free不允许重载
    • 已分配内存的扩充. malloc/free可以通过realloc函数扩充,new/delete无法直观地处理
    • 能否相互调用. operator_new /operator _delete的实现可以基于malloc/free,而malloc的实现不可以去调用new。

    new运算符的原理

    • 内存分配

      • 调用相应的 operator new(size_t) 函数,动态分配内存。如果 operator new(size_t) 不能成功获得内存,则调用 new_handler()函数用于处理new失败问题。如果没有设置 new_handler() 函数或者 new_handler() 未能分配足够内存,则抛出 std::bad_alloc 异常
    • 构造函数

      • 在分配到的动态内存块上 初始化相应类型的对象(构造函数)并返回其首地址。如果调用构造函数初始化对象时抛出异常,则自动调用 operator delete(void*, void*) 函数释放已经分配到的内存。

    malloc的内存分配机制

    malloc内存分配机制是怎么样的,在哪里分配内存,最大可以申请多大的内存?

    • 首先会扫描之前由free()所释放的空闲内存块列表,以求找到尺寸大于或等于要求的一块空闲内存。如果这一内存块的尺寸正好与要求相当,就将它返回给调用者,如果是一块较大的内存,那么将对其进行分割,在将一块大小相当的内存返回给调用者的同时,把较小的那块空闲内存块保留在空闲列表中

    栈和堆的区别

    • 管理方式不同
      • 对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。
    • 空间大小不同
      • 一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M
    • 能否产生碎片不同
      • 对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出
    • 生长方向不同
      • 对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长

    面向对象和面向过程的区别

    面向对象就是高度实物抽象化、面向过程就是自顶向下的编程

    • 面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用
    • 面向对象是把要解决的问题分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为
    • 面向过程的优缺点
      1. 优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
      2. 缺点:没有面向对象易维护、易复用、易扩展
    • 面向对象的优缺点
      1. 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护
      2. 缺点:性能比面向过程低

    const关键字(不可修改)

    • 修饰变量,说明该变量不可以被改变,const全局变量还会使该全局变量作用域仅限于该文件,起到"static"的作用;const 全局变量存放在只读数据段,const局部变量存放在栈上。
    • 修饰指针,分为指向常量的指针和指针常量;int *const p和const int *p
    • 常量引用,经常用于形参类型,即避免了拷贝,又避免了函数对值的修改;
    • 修饰成员函数
      • 成员函数后面+const:该成员函数内不能修改成员变量(static成员除外)
      • 成员函数中形参+const:表示修饰的形参在其函数体不能被修改
      • 成员函数前+const:表示其返回值不能被修改
    • const修饰成员变量
      • 静态成员变量:类外对该变量进行初始化
      • 非静态成员变量:构造函数使用初始化列表进行初始化

    static关键字(对外不可见)

    • 修饰普通变量, 修改变量的存储区域和生命周期,使变量存储在静态区,在 main 函数运行前就分配了空间,在整个程序运行期间一直存在,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它,自动初始化为0。
      全局变量作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。
      局部变量作用域:仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;
    • 修饰普通函数, 其只能在定义它的源文件中使用,不能在其他源文件中被引用
    • 修饰类成员变量和成员函数, 它们是属于类的,而不是某个对象,所有对象共享一个静态成员。静态成员通过<类名>::<静态成员>来使用。在 static 函数内不能访问非静态成员

    extern关键字

    extern关键字主要修饰变量或函数,表示该函数可以跨文件访问,或者表明该变量在其他文件定义,在此处引用.

    • **修饰变量或函数.**被 extern 限定的函数或变量是 extern 类型的
    • extern “C”. extern “C” 的作用是让 C++ 编译器将 extern “C” 声明的代码当作 C 语言代码处理,可以避免 C++ 因符号修饰导致代码不能和C语言库中的符号进行链接的问题。
    1. 在c头文件中通过#ifdef __cplusplus extern “C” { #endif来定义
    2. 在对应的c文件中实现
    3. 在cpp文件中通过“extern “C” 函数名”调用,或者包含c头文件
      注意: extern的引用方式比包含头文件要简洁得多!extern的使用方法是直接了当的,想引用哪个函数就用extern声明哪个函数。这样做的一个明显的好处是,会加速程序的编译(确切的说是预处理)的过程,节省时间,但若需要调用的函数太多,还是直接包含头文件吧.

    volatile关键字

    • 不可优化性. volatile 关键字是一种类型修饰符,用它声明的类型变量表示不可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用 volatile 告诉编译器不应对这样的对象进行优化。
    • 易变性. volatile 关键字声明的变量,每次访问时都必须从内存中取出值(没有被 volatile 修饰的变量,可能由于编译器的优化,从 CPU 寄存器中取值)

    explicit关键字

    explicit关键字的作用就是防止对象间实现 = 赋值(赋值构造函数),防止类构造函数的隐式自动转换,类构造函数默认情况下即声明为implicit(隐式),

    另外explicit只用于单参数的构造函数,或者除了第一个参数以外的其他参数都有默认值.

    • explicit 修饰构造函数时,可以防止隐式转换和复制初始化
    • explicit 修饰转换函数时,可以防止隐式转换
    class Person{
    public:
        Person(){
        }
        //有参构造初始化数据
        explicit Person(const char* str_){
            str = (char *)malloc(sizeof(char)*100);
            strcpy(str,str_);
        }
        ~Person() {
            if (str != NULL){
                free(str);
                str = NULL;
            }
        }
        char *str;
    };
    void test05(){
        //Person p = "abc"; 隐式调用
        Person p ("abc"); //显式调用
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    类成员属性

    类的成员有三个权限:公有权限(public),私有C权限(private),保护权限(protected)。

    • 私有权限. 私有成员在类内部可以访问,类外不可访问,一般推荐将成员变量设置为私有成员;
    • 公有权限. 类内类外都可以进行访问;
    • 保护权限. 类内和当前类的子类可以访问,类外不可访问。

    大小端序的定义和代码判断

    定义

    一个16进制的地址,存放在内存中从低地址开始存储,如16进制的地址为0x1234,对于地址而言,从右往左是从低到高

    • 大端
      • 若16进制的低地址存放在内存的高地址,则为大端字节序,34存储在高位,12存储在低位
    • 小端
      • 若16进制的低地址存放在内存的低地址,则为小端字节序,12存储在高位,34存储在低位

    代码判断

    可以通过联合体来判断,联合体是同一块内存被联合体中的所有成员公用,如果后续成员对内存重新赋值,会覆盖内存中原有数据

    union U{
    	int a;
    	char b;
    };
    
    int main(){
    	U u;
    	u.a = 0x01020304;
    
    	if (u.b == 0x04){
    		cout << u.b << endl; //输出char字符
    		cout << "little" << endl;
    	}
    
    	else if (u.b == 0x01){
    		cout << u.b << endl;
    		cout << "big" << endl;
    	}
    	system("pause");
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    代码判断32位和64位系统

    写一个指针,输出指针所占的字节大小

    线程A和线程B不加锁,对同一个变量,各自执行 i++ 100次,结果是啥

    最小值2,最大值200。

    最小值2

    • 初始条件,内存值为0
    • A线程从内存中取出值,进行第一次++操作,还未写回内存就被挂起,此时A寄存器 1 ,内存 0 ;
    • B线程从内存中取出值,进行第一次++操作,还未写回内存就被挂起,此时B寄存器 1 ,内存 0 ;
    • A连续执行到第99次++操作,每次都写回到内存,此时A寄存器 99 ,内存 99 ;
    • B继续执行,将自己寄存器的值写回到内存,覆盖了内存中的99,此时B寄存器 1 ,内存 1 ;
    • A执行第100次++操作,取出内存中的值1,然后++,还未写回内存就被挂起,此时A寄存器 2 ,内存 1 ;
    • B执行完自己所有剩余操作,此时B寄存器为100, 内存为100;
    • A继续执行,将自己的寄存器的值写回内存,内存为2。

    最大值200

    每个线程的每次执行都将值写回内存

    C++指定内存分配对象

    placement new

    首先分配足够大的内存;然后用placement new语法生成对象:new(ptr) xxx(),其中ptr是足够容纳所指对象的指针。

    class Person {
    private:
        int age;
        std::string name;
    public:
      // methods
    };
     
    int main(int argc, char** argv) {
        auto mem = malloc(sizeof(Person));
        auto p = new(mem) Person();
        p->show();
        free(mem);
        return 0;
    }
    /*
    Person
    Lily's age is 10
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    allocator

    先分配内存,然后在其上装载对象,可以使用allocatorallocator定义在头文件中,能对指定类型分配合适的内存,并可手动调用对象的构造函数和析构函数。

    class Person {
    private:
      int age;
      std::string name;
    public:
        Person() {
            age = 10;
            name = "Lily";
            cout << "Person" << endl;
        }
        void show() {
            cout << name << "'s age is " << age << endl;
        }
        ~Person() {
            cout << "~Person" << endl;
        }
    };
    
    int main() {
      std::allocator alloc;
      auto p = alloc.allocate(1); // 分配一个Person对象的内存
      alloc.construct(p);   // 调用Person的构造函数,如果构造函数有参数,参数写在p之后
      // p 现在是一个指向Person的指针,且其指向对象被初始化过
      // 对p进行一些操作
        p->show();
      // 销毁对象,但不释放内存,等同于调用p->~Person()
      alloc.destroy(p);
      // 释放内存
      alloc.deallocate(p, 1);
      return 0;
    }
    /*
    Person
    Lily's age is 10
    ~Person
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
  • 相关阅读:
    EA&UML日拱一卒 用例包含关系
    SequenceFile、元数据操作与MapReduce单词计数
    Spring-AOP
    Mac M1安装Docker---kalrry
    QDebug 日志输出的浏览器
    122. 买卖股票的最佳时机 II
    Python基础入门篇【31】--面向对象:类中的私有函数与私有变量
    Java - SpringBoot整合Shiro之二(权限授权和认证跳过)
    SPark学习笔记:12 Spark Streaming 的简单应用WordCount
    算法通关村第18关【青铜】| 回溯
  • 原文地址:https://blog.csdn.net/Runnymmede/article/details/132724747