C++基础知识

1 static关键字

1.1 面向过程的static

1.1.1 全局静态变量

  • 声明使用:在全局变量前,加上关键字static,该变量就被定义成为一个静态全局变量;
  • 存储位置:静态全局变量存储在全局静态储存区,全局静态存储区在程序运行期间一直存在,即main函数执行期间全局静态存储区的数据不会被系统销毁回收空间;
  • 初始化:未经初始化的全局静态变量会被默认初始化为0;
  • 可见性:全局静态变量在整个文件中都是可见的,但是不被其他文件可见,因此其他文件不能extern一个静态变量,其他文件可以重新定义一个相同名字的变量。

1.1.2 局部静态变量

  • 声明使用:在局部变量前,加上关键字static,该变量就被定义成为一个静态局部变量;
  • 存储位置:与全局静态变量一样,存储在全局静态存储区;这样有一个好处就是,但局部变量不再被使用时,并不会被系统回收内存,因此其可以一直被保留,直到程序运行结束。我们常见的一种情况是,我们需要在一个函数中保留某个局部变量的值(因为函数可能被重复调用,但要保留上一次的信息),这时不得不使用全局变量或者入口参数进行保存。而静态局部变量完美解决这个问题,因为静态局部变量声明定义语句只执行一次,并且不会被回收内存,所以解决上述问题比较方便;
  • 初始化:未经初始化的局部静态变量会被默认初始化为0;
  • 可见性:与局部变量定义域相类似,只不过当定义域结束,内存不会被回收,仍然能被保留,同样只能在一个文件中可见。

1.1.3 静态函数

  • 声明使用:在函数的返回类型前加上static关键字,函数即被定义为静态函数;
  • 可见性:静态函数与普通函数不同,它只能在声明它的文件当中可见,不能被其它文件使用;
  • 好处:只能在定义声明它的文件中使用,在其他文件定义不会有名字冲突。

1.2 面向对象的static

1.2.1 类中的静态成员

  • 声明使用:在类内数据成员的声明前加上关键字static,该数据成员就是类内的静态数据成员;
  • 特点:
    1.静态数据成员是属于类的,非静态数据成员是属于对象的。也就是说,对于非静态数据成员,在实例化一个类对象时,非静态数据成员都会进行拷贝一次,只供实例化出来的类对象使用;而对于静态数据成员来说,只进行分配一次内存,所有类对象共享这一块内存,并不单独属于某一个类对象,而是属于这个类的所有对象;
    2.静态数据成员是存储在全局数据区,因此,其不能在类声明的时候进行初始化定义;
    3.静态数据成员和普通数据成员一样遵从public,protected,private访问规则;
    4.由于静态数据成员是在全局数据区,其在类没有实例化之前就已经初始化存在,因此我们在没有实例化类之前就可见,并且可操作它;
    5.静态数据成员初始化与一般数据成员初始化不同。静态数据成员初始化的格式为:<数据类型><类名>::<静态数据成员名>=<值>;
    6.类的静态数据成员有两种访问形式:<类对象名>.<静态数据成员名> 或 <类类型名>::<静态数据成员名>;
  • 优势:
    1.静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性;
    2.可以实现信息隐藏。静态数据成员可以是private成员,而全局变量不能;

1.2.2 类中的静态成员函数

  • 说明:与静态数据成员一样,我们也可以创建一个静态成员函数,它为类的全部服务而不是为某一个类的具体对象服务。静态成员函数与静态数据成员一样,都是类的内部实现,属于类定义的一部分。普通的成员函数一般都隐含了一个this指针,this指针指向类的对象本身,因为普通成员函数总是具体的属于某个类的具体对象的。通常情况下,this是缺省的。如函数fn()实际上是this->fn()。但是与普通函数相比,静态成员函数由于不是与任何的对象相联系,因此它不具有this指针。从这个意义上讲,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数。
  • 总结为以下几点:
    1.出现在类体外的函数定义不能指定关键字static;
    2.静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数;
    3.非静态成员函数可以任意地访问静态成员函数和静态数据成员;
    4.静态成员函数不能访问非静态成员函数和非静态数据成员;
    5.由于没有this指针的额外开销,因此静态成员函数与类的全局函数相比速度上会有少许的增长;
    6.调用静态成员函数,可以用成员访问操作符(.)和(->)为一个类的对象或指向类对象的指针调用静态成员函数,也可以直接使用如下格式:
    <类名>::<静态成员函数名>(<参数表>)
    调用类的静态成员函数。

2 说一下C++与C的区别

  • 主要区别:C语言是面向结构的语言, C++是面向对象语言,C++已经从根本上发生了质的飞跃,对C进行了丰富的扩展;
  • 语法上的区别:
1. C++具有封装、继承和多态三种特性
2. C++相比C,增加多许多类型安全的功能,比如强制类型转换
3. C++支持范式编程,比如模板类、函数模板等
4. 不能用""来代替<>包含头文件
5. main()函数的返回值不能设置为void,一般设置为int main(void)  void表示不需要从命令行获取参数;如果需要则int main(int argc,char *argv[])。
6. 如果头文件是C++持有,则去掉.h后缀,并放入std命名空间,比如iostream;如果这个头文件是C持有的,去掉.h并在前面加上c字符,如cstring;注意头文件不要搞混淆

3 说一说C++四种cast转换

c风格的类型转换统一为 TYPE B = (TYPE)A,而C++种这种语法标识太多,并且应对的情况也很多,所以C++针对不同的情况,引入了4种新的类型转换操作符 static_cast、const_cast、dynamic_cast、reinterpret_cast

  • static_cast
1. 代替隐式转换,
	a.void*转换为任意类型的指针;b.任意指针转为void*;
	c.编译器允许的跨类型转换,比如char类型转为int类型,double转int型;
2. 做基类与派生类之间的转换,
	a.派生类转换为基类是安全的;
	b.基类转换为派生类是不安全的,结果未知。这是由于派生类的成员一般比基类要多造成的。
  • const_cast
1. const_cast用于去除const(volatile)属性,将只读变为可读写;
2. const_cast只针对指针、引用、this指针,其他情况会出错。
  • dynamic_cast
1. dynamic_cast和static_cast的效果是一样的;在进行下行转换时,
	dynamic_cast具有类型检查的功能,弥补了static_cast类型不安全的缺陷,比static_cast更安全
2. 多用于有虚函数的基类与其派生类之间的转换,特点是进行运行时检测转换类型是否安全,
	如果转换失败返回nullptr,依赖于RTTI技术,但是有额外的函数开销,所以非必要的时候不使用。

RTTI是一种意思是运行时类型信息,它提供了运行时确定对象类型的方法,换句话说,RTTI是一种可以获取变量在运行时的实际指向的机制,使用了typeid()函数

  • reinterpret_cast
1. reinterpret代替显示转换,用于转换各种高风险的转换(隐式转换无法转换的)
2. 不进行类型检查,只进行强制复制,有安全隐患
3. 是四种转换种功能最为强大的。

4 C/C++中引用和指针的区别

  1. 指针有自己独立的内存空间,引用只是一个别名(对于引用是否占内存空间有争议,应该是根据编译器来决定,但是C++设计的本意是其不占用内存空间);
  2. sizeof调用,指针一般占4个字节,而引用在用sizeof时,显示的是被引用的变量的实际占用内存大小;
  3. 指针可以被初始化为NULL,而引用必须是对一个实例的引用;
  4. 有const指针,但是没有const引用;
  5. 有多级指针 **p,但没有多级引用;
  6. 指针需要解引用才能操作对象,但是引用相当于别名可直接进行操作;
  7. 作为返回值,应该用指针,而不应采用引用,采用引用容易造成内存泄露。

5 说一说C++中的智能指针

C++中共有4个智能指针, 分别是auto_ptr、shared_ptr、unique_ptr、weak_ptr,目前auto_ptr已经被C++11标准废除

为什么要使用智能指针?
1. 动态内存的管理是通过一堆运算符new和delete来完成的。但是在保证动态内存在合适的时间释放是一件极其困难的事情,所以C++标准库提供了智能指针来进行管理动态对象。智能指针的行为类似于一般的指针,重要的区别就是其能自动的释放所指向的对象。对于shared_ptr、unique_ptr、weak_ptr都定义在memory头文件中

2. 智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。

  • auto_ptr
    C++98的方案,C++11已经废弃。auto_ptr采用所有权方式,其缺点是存在潜在的内存崩溃问题

  • unique_ptr(用来替换auto_ptr)
    unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。对于临时右值编译器允许将unique_ptr赋值。注意我们虽然不能对unique_ptr进行拷贝或者赋值操作,但我们能通过release或者reset将一个unique_ptr的所有权进行转移给另一个unique_ptr

  • shared_ptr
    shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。

  • weak_ptr
    weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

6 智能指针有没有内存泄露的情况?如何解决?

  • 当两个对象互相使用shared_ptr指向对方,会造成循环引用,导致引用计数失效,从而导致内存泄露。
  • 解决方法:
    为了解决循环引用导致的内存泄漏,引入了weak_ptr弱指针,weak_ptr的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但不指向引用计数的共享内存,但是其可以检测到所管理的对象是否已经被释放,从而避免非法访问。

7 为什么构造函数不能是虚函数?析构函数必须设置为虚函数?

1 构造函数不能是虚函数

  • 虚函数表vtable是在对象实例化出来之后才进行简历的,而虚函数的调用是通过查vtable来进行调用。对于构造函数来讲,如果将其设计为虚函数,则相当于在对象未被实例化的时候进行调用虚函数,从实现机制上不支持。
  • 对于构造函数来说,执行顺序是先实例化基类然后再实例化派生类。这样看由于将构造函数设计为虚函数会造成在执行的时候不能确定该类是基类还是派生类亦或者是派生类的派生类,也就无法进行动态绑定。

2 析构函数被设置为虚函数

  • 为什么C++中默认的析构函数不是虚函数? C++中默认的析构函数不是虚函数,这是因为虚函数表和虚表指针会占用额外的空间。
  • 虚函数是在类结束生命周期自动调用,并且规定析构函数只会释放该类的成员,所以需要虚函数来递归的将父类的成员释放。
  • 释放顺序为派生类->父类。

对于在析构函数或者构造函数中调用了虚函数,我们会执行与析构函数或者构造函数相符合的类的虚函数版本

Tags: