基础语法

1、在main执行之前和之后执行的代码可能是什么?

  • attribute((constructor))在main之前调用
  • attribute((destructor))在main之后调用
  • atexit()是C 库函数 int atexit(void (*func)(void)) 当程序正常终止时,调用指定的函数 func。可以在任何地方注册你的终止函数,但它会在程序终止的时候被调用。

    2、结构体内存对齐

    结构体内{
    int a:8;
    int b:2;
    int c:6;
    };
    位域,”位域”是把一个字节中的二进位划分为几个不同的区域, 并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。
    第一个成员在与结构体偏移量为0的地址处。
    其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
    VS中默认的对齐数为8
    结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
    如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是
    所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

    7、区别指针数组和数组指针,函数声明和函数指针

    区别指针数组和数组指针
    下面到底哪个是数组指针,哪个是指针数组呢:
    A)
    int p1[10];
    B)
    int (
    p2)[10];
    每次上课问这个问题,总有弄不清楚的。这里需要明白一个符号之间的优先级问题。
    “[]”的优先级比“”要高。p1 先与“[]”结合,构成一个数组的定义,数组名为p1,int 修饰的是数组的内容,即数组的每个元素。那现在我们清楚,这是一个数组,其包含10 个指向int 类型数据的指针,即指针数组。至于p2 就更好理解了,在这里“()”的优先级比“[]”高,“*”号和p2 构成一个指针的定义,指针变量名为p2,int 修饰的是数组的内容,即数组的每个元素。数组在这里并没有名字,是个匿名数组。那现在我们清楚p2 是一个指针,它指向一个包含10 个int 类型数据的数组,即数组指针。我们可以借助下面的图加深理解:
    image.png

    8、new / delete 与 malloc / free的异同

    new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void ,需要通过强制类型转换将void指针转换成我们需要的类型。类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图方法自己没被授权的内存区域。

    12、被free回收的内存是立即返还给操作系统吗?

    因为brk、sbrk、mmap都属于系统调用,若每次申请内存,都调用这三个,那么每次都会产生系统调用,影响性能;其次,这样申请的内存容易产生碎片,因为堆是从低地址到高地址,如果高地址的内存没有被释放,低地址的内存就不能被回收。  
      所以malloc采用的是内存池的管理方式(ptmalloc),Ptmalloc 采用边界标记法将内存划分成很多块,从而对内存的分配与回收进行管理。为了内存分配函数malloc的高效性,ptmalloc会预先向操作系统申请一块内存供用户使用,当我们申请和释放内存的时候,ptmalloc会将这些内存管理起来,并通过一些策略来判断是否将其回收给操作系统。这样做的最大好处就是,使用户申请和释放内存的时候更加高效,避免产生过多的内存碎片。

    14、宏定义和typedef区别?

    define和typedef对于指针
    宏定义只是简单的字符串代换(原地扩展),而typedef则不是原地扩展,它的新名字具有一定的封装性,以致于新命名的标识符具有更易定义变量的功能。请看上面第一大点代码的第三行:
    typedef (int) pINT;
    以及下面这行:
    #define pINT2 int

    效果相同?实则不同!实践中见差别:pINT a,b;的效果同int a; int b;表示定义了两个整型指针变量。而pINT2 a,b;的效果同int *a, b;表示定义了一个整型指针变量a和整型变量b。

    20、C++和C语言的区别

    全新的程序程序思维,C语言是面向过程的,而C++是面向对象的。

    28、拷贝初始化和直接初始化

    使用explicit修饰构造函数时:如果构造函数存在隐式转换,编译时会报错.
    被声明为explicit的构造函数通常比其 non-explicit 兄弟更受欢迎, 因为它们禁止编译器执行非预期 (往往也不被期望) 的类型转换.

    29、初始化和赋值的区别

    赋值操作是在两个已经存在的对象间进行的,而初始化是要创建一个新的对象,并且其初值来源于另一个已存在的对象。编译器会区别这两种情况,赋值的时候调用重载的赋值运算符,初始化的时候调用拷贝构造函数。如果类中没有拷贝构造函数,则编译器会提供一个默认的。这个默认的拷贝构造函数只是简单地复制类中的每个成员。

    33、C++中的重载、重写(覆盖)和隐藏的区别

    // 基类指针指向派生类对象时,基类指针可以直接调用到派生类的覆盖函数,也可以通过 :: 调用到基类被覆盖 // 的虚函数;而基类指针只能调用基类的被隐藏函数,无法识别派生类中的隐藏函数。

    34、C++有哪几种的构造函数

    为什么引入右值引用,引入移动构造函数?
    引入右值引用的主要目的是提高程序运行的效率。当类持有其它资源时,例如动态分配的内存、指向其他数据的指针等,拷贝构造函数中需要以深拷贝(而非浅拷贝)的方式复制对象的所有数据。深拷贝往往非常耗时,合理使用右值引用可以避免没有必要的深拷贝操作。
    这里引入了移动语义,所谓移动语义(Move语义),指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。对于程序执行过程中产生的临时对象,往往只用于传递数据(没有其它的用处),并且会很快会被销毁。因此在使用临时对象初始化新对象时,我们可以将其包含的指针成员指向的内存资源直接移给新对象所有,无需再新拷贝一份,这大大提高了初始化的执行效率。
    //移动构造函数
    A(A&& a) : x(a.x) {
    cout << “Move Constructor” << endl;
    }
    委托构造函数
    委托构造函数
    委托构造函数(Delegating Constructor)由C++11引入,是对C++构造函数的改进,允许构造函数通过初始化列表调用同一个类的其他构造函数,目的是简化构造函数的书写,提高代码的可维护性,避免代码冗余膨胀。
    通俗来讲,一个委托构造函数使用它所属的类的其他构造函数执行自己的初始化过程,或者说它把自己的一些(或者全部)职责委托给了其他构造函数。和其他构造函数一样,一个委托构造函数也有一个成员初始化列表和一个函数体,成员初始化列表只能包含一个其它构造函数,不能再包含其它成员变量的初始化,且参数列表必须与构造函数匹配。

    38、用代码判断大小端存储

    使用强制类型转换-留下低址部分看内容
    union联合体的重叠式存储,endian联合体占用内存的空间为每个成员字节长度的最大值

    40、什么情况下会调用拷贝构造函数

    不优化的返回值result是通过copy ctor复制xx对象构造的,而优化后返回值result是通过default ctor构造的。
    NRV优化的本质是优化掉拷贝构造函数。在实现模型1中我们看到了,返回对象的实现总是先对某个对象进行操作,操作完成后,使用Copy Constructor将操作后的对象内容复制到另外一个对象中,然后返回。 基于这样一个前提,NRV可以调用Copy Constructor这一步被省掉。所以NRV的前提必须要有Copy Constructor。
    编译器自己构造了一个与返回值同样类型的对象来实现return by value。
    在c++编译器发生NRV优化,如果是引用返回的形式则不会调用拷贝构造函数,如果是值传递的方式依然会发生拷贝构造函数。

    61、什么是内存泄露,如何检测与避免

    将基类的析构函数声明为虚函数可以避免内存泄露?
    在基类中将析构函数声明为虚函数,就表示在使用析构函数时,是采用运行期绑定的。那么delete basePtr的时候运行的是根据basePtr指向的具体类型来调用析构函数。这样派生类和基类的析构函数都会被调用,否则只有基类的析构函数被调用。
    基类指针指向派生类的时候,如果析构函数声明为虚函数,在析构的时候,不会调用派生类的析构函数,从而导致内存泄露
    C运行时库就是 C run-time library,CRT

    65、C++的四种强制转换reinterpret_cast/const_cast/static_cast /dynamic_cast

    C++四种强制转换

    76、方法的调用机制原理

    1、当运行的方法的时候,就会独立开辟一个独立的栈空间。
    2、main单独一个栈,中间再在堆中创建对象。
    3、类的命名规则区别于方法的命名规则和变量名的命名规则。
    4、返回以后,临时的栈(方法的栈)就没有了。
    5、传递参数的时候要有返回值可以一起定义一个变量来接收。
    image.png
    1、当程序执行到方法时,就会开辟一个独立的栈空间。
    2、当一个方法执行完毕,或者执行到return语句时,就会返回。
    3、就会返回调用方法的位置。
    4、返回后继续执行后面的代码。输出语句执行之后。
    5、若main(栈)执行结束了,整个程序就结束啦。

    78、类如何实现只能静态分配和只能动态分配

    1、只能动态分配
    类对象只能通过new运算符建立在堆空间中,不能静态分配,即不能直接调用类的构造函数
    怎样才能实现类的构造函数不能被直接调用呢?首先想到的就是将类的构造函数设为private,这样就无法在类外部调用构造函数来构造对象了,只能使用new运算符。但是,这种做法行不通。为什么???因为前边已经说过,new运算符过程分为两步,C++提供new运算符的重载,其实只允许重载operator new( )函数,而operator()函数用于内存分配,无法提供构造函数功能。因此,将构造函数设为private不可行。
    从另外一个角度分析,对象静态分配时,是由编译器负责分配内存空间的,调用构造函数在栈空间中构造对象。当对象使用完毕,编译器又会调用析构函数来释放栈空间中的类对象。如果编译器无法调用类的析构函数,会怎样呢?其实,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性(其实不光是析构函数,只要是非静态的函数,编译器都会进行检查)。如果类的析构函数在类外部无法访问,则编译器拒绝在栈空间上为类对象分配内存。
    因此,可以将析构函数设为private,这样就无法在栈上建立类对象了。
    这样在使用A a; 来建立对象时会编译错误,提示析构函数无法访问。这样就只能用new运算符来建立对象了,并且构造函数可以调用,因为是public。但是必须提供一个destory函数来实现内存空间的释放。
    上面的方法虽然能实现只能动态建立对象,但是有一个缺点:无法实现继承。因为如果A作为基类的话,则析构函数通常要设为virtual,然后在子类中被重写,以实现多态。因此析构函数不能设为private。为了解决这个问题该怎么办呢?别忘了,C++还提供了第三种访问控制,就是protected。将析构函数设为protected,类外无法访问protected成员,但是子类可以访问。
    2、只能静态分配
    只有使用new运算符,对象才会被建立在堆上。因此只要限制new运算符就可以实现类对象只能建立在栈上。可以将new运算符设为私有,

    79、如果想将某个类用作基类,为什么该类必须定义而非声明?

    如果我们想用某个类作为基类,则该类必须已经定义而非仅仅声明。
    这一规定的原因显而易见:派生类中包含并且可以使用它从基类继承而来的成员,为了使用这些成员,派生类当然要知道它们是什么。因此该规定还有一层隐藏的意思,即一个类不能派生它本身。

    83、说一说你理解的内存对齐以及原因

    为什么要进行内存对齐?
    (1) 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
    (2) 平台原因:不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
    (3) 空间原因:没有进行内存对齐的结构体或类会浪费一定的空间,当创建对象越多时,消耗的空间越多。
    在 C/C++ 中,结构体/类是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构、联合等)的数据单元。编译器为每个成员按其自然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。
    如果一个变量的内存地址正好位于它长度的整数倍,他就被称做自然对齐。

    104、C++中标准库是什么?

    C++ 标准库可以分为两部分:

  • 标准函数库: 这个库是由通用的、独立的、不属于任何类的函数组成的。函数库继承自 C 语言。

  • 面向对象类库: 这个库是类及其相关函数的集合。

C++ 标准库包含了所有的 C 标准库,为了支持类型安全,做了一定的添加和修改。
标准函数库
标准函数库分为以下几类:

  • 输入/输出 I/O
  • 字符串和字符处理
  • 数学
  • 时间、日期和本地化
  • 动态分配
  • 其他
  • 宽字符函数

面向对象类库
标准的 C++ 面向对象类库定义了大量支持一些常见操作的类,比如输入/输出 I/O、字符串处理、数值处理。面向对象类库包含以下内容:

using namespace std;

template class Shared_Ptr{ public: //构造 Shared_Ptr(T ptr=nullptr):_ptr(ptr){ if(ptr){ _count=new size_t(1); }else{ _count=new size_t(0); } } //拷贝构造 Shared_Ptr(const Shared_Ptr& src){ if(this!=&src){ _ptr=src._ptr; _count=src._count; (_count)++; } } //赋值 Shared_Ptr& operator=(const Shared_Ptr& src){ if(_ptr==src._ptr){ return this; } if(_ptr){ (_count)—; if(_count==0){ delete _ptr;_ptr=NULL; delete _count;_count=NULL; } } _ptr=src._ptr; _count=src._count; (_count)++; return *this; }

  1. T& operator*() const{
  2. return *_ptr;
  3. }
  4. T* operator->() const{
  5. return *_ptr;
  6. }
  7. //看计数,其实没有
  8. size_t use_count(){
  9. return *_count;
  10. }
  11. //析构
  12. ~Shared_Ptr(){
  13. (*_count)--;
  14. if(*_count==0){
  15. delete _ptr;_ptr=NULL;
  16. delete _count;_count=NULL;
  17. }
  18. }

private: T _ptr; size_t _count;//指向引用计数的指针 };

int main(){ Shared_Ptr sp1(new int(10)); cout<<sp1.use_count()<<endl;

Shared_Ptr<int> sp2(sp1);
cout<<sp1.use_count()<<endl;
cout<<sp2.use_count()<<endl;

Shared_Ptr<int> sp3;
sp3=sp2;
cout<<sp1.use_count()<<endl;
cout<<sp2.use_count()<<endl;
cout<<sp3.use_count()<<endl;

return 0;

} ```

STL

其他