C++——基础语法
一. 函数执行
1. 在main执行之前和之后执行的代码可能是什么?
main函数执行之前,主要就是初始化系统相关资源:
- 设置栈指针
- 初始化静态static变量和global全局变量,即.data段的内容
- 将未初始化部分的全局变量赋初值:数值型short,int,long等为0,bool为FALSE,指针为NULL等等,即.bss段的内容
- 全局对象初始化,在main之前调用构造函数,这是可能会执行前的一些代码
- 将main函数的参数argc,argv等传递给main函数,然后才真正运行main函数
attribute((constructor))
main函数执行之后:
- 全局对象的析构函数会在main函数之后执行;
- 可以用 atexit 注册一个函数,它会在main 之后执行;
- attribute((destructor))
二. 指针、变量、引用和this指针
1. 指针和引用的区别
- 指针是一个变量,存储的是一个地址;引用是原变量的别名。
- 指针可以为空,引用不能为空且在定义时必须初始化。
- 指针在初始化后可以改变指向,而引用在初始化之后不可再改变。
- 指针可以有多级,引用只有一级
2. 传递函数参数时,何时使用指针?何时使用引用
使用指针的情况:
- 需要返回函数内局部变量的内存的时候用指针。使用指针传参需要开辟内存,用完要记得释放指针,不然会内存泄漏。
使用引用的情况:
- 对栈空间大小比较敏感(比如递归)的时候使用引用。使用引用传递不需要创建临时变量,开销要更小。
- 类对象作为参数传递的时候使用引用,这是C++类对象传递的标准方式
3. this指针
1. this指针的用处
this 是 C++ 中的一个关键字,也是一个 const 指针,它指向当前对象,通过它可以访问当前对象的所有成员。
2. this指针的使用范围
this指针只能在成员函数中使用,在全局函数、静态成员函数中都不能用this。
三. 栈、堆、队列
1. 栈和队列的区别
- 出入方式不同:栈是后进先出、队列是先进先出。
- 栈和队列在具体实现的时候操作的位置不同:因为栈是后进先出,它在一端进行操 作;而队列是先进先出,实现的时候在两端进行。
- 遍历数据速度不同:栈只能从头部取数据,也就最先放入的需要遍历整个栈最后才能取出来,而且在遍历数据的时候还得为数据开辟临时空间,保持数据在遍历前的一致性。队列则不同,它基于地址指针进行遍历,而且可以从头或尾部开始遍历,但不能同时遍历,无需开辟临时空间,因为在遍历的过程中不影像数据结构,速度要快的多
2. 栈和堆的区别
- 申请方式:栈由系统自动分配;堆是自己申请和释放的(容易产生memory leak)。
- 空间大小:栈是一块连续的内存区域,大小是操作系统预定好的,windows下栈大小是2M;堆是不连续的内存区域,堆大小受限于计算机系统中有效的虚拟内存,32bit 系统理论上是4G。
- 生长方向:栈向下,向低地址方向增长;堆向上,向高地址方向增长。
- 分配效率:栈是其系统提供的数据结构,计算机在底层对栈提供支持,分配专门 寄存器存放栈地址,栈操作有专门指令,所以栈的效率比较高也比较快。堆的操作是由C/C++函数库提供的,在分配堆内存的时候需要一定的算法寻找合适大小的内存。并且获取堆的内容需要两次访问,第一次访问指针,第二次根据指针保存的地址访问内存,因此堆比较慢。
- 碎片问题:栈数据进出一一对应,不会产生碎片。堆频繁的new/delete会造成大量碎片,使程序效率降低。
3. new/delete和malloc/free的区别
- malloc和free是标准库函数,支持覆盖; new和delete是运算符,支持重载。
- malloc仅仅分配内存空间,free仅仅回收空间,不具备调用构造函数和析构函数功能,用malloc分配空间存储类的对象存在风险;new和delete除了分配回收功能外,还会调用构造函数和析构函数。
- new自动计算要分配的空间大小,malloc需要手工计算。
- new是类型安全的,malloc不是。
四. 关键字的区别
1. struct和class的区别
- 相同点:
(1)两者都拥有成员函数、公有和私有部分
(2)任何可以使用class完成的工作,同样可以使用struct完成 - 不同点:
(1)struct里面的成员默认是公有的,且为公有继承;class默认是私有的,且为私有继承;
2. const和static的区别
static:
- 不考虑类的情况:
(1)隐藏作用:所有不加static的全局变量和函数具有全局可见性,可以在其他文件中使用,加了之后只能在该文件所在的编译模块中使用;
(2)默认初始化为0,包括未初始化的全局静态变量与局部静态变量,都存在全局未初始化区;
(3)静态变量在函数内定义,始终存在,且只进行一次初始化,具有记忆性,其作用范围与局部变量相同,函数退出后仍然存在,但不能使用。 - 考虑类的情况:
(1)static成员变量:只与类关联,不与类的对象关联。定义时要分配空间,不能在类声明中初始化,必须在类定义体外部初始化,初始化时不需要标示为static;可以被非static成员函数任意访问。
(2)static成员函数:不具有this指针,无法访问类对象的非static成员变量和非static成员函数;不能被声明为const、虚函数和volatile;可以被非static成员函数任意访问
const:
1.不考虑类的情况:
(1)const常量在定义时必须初始化,之后无法更改。
(2)const形参可以接收const和非const类型的实参。
2.考虑类的情况:
(1)const成员变量:不能在类定义外部初始化,只能通过构造函数初始化列表进行初始化,并且必须有构造函数;不同类对其const数据成员的值可以不同,所以不能在类中声明时初始化。
(2)const成员函数:const对象不可以调用非const成员函数;非const对象都可以调用;不可以改变非mutable(用该关键字声明的变量可以在const成员函数中被修改)数据的值;
(3)const修饰变量是也与static有一样的隐藏作用。只能在该文件中使用,其他文件不可以引用声明使用。 因此在头文件中声明const变量是没问题的,因为即使被多个文件包含,链接性都是内部的,不会出现符号冲突。
五. C和C++的类型安全
1. 什么是类型安全?
类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图访问自己没被授权的内存区域。
2. C++的类型安全
C++提供了一些机制保障类型安全:
(1)操作符new返回的指针类型严格与对象匹配。
(2)C++提供了dynamic_cast关键字,使得转换过程更加安全,因为dynamic_cast比static_cast涉及更多具体的类型检查。
六. C++中的重载、重写(覆盖)和隐藏的区别
1. 重载
重载是指在同一范围定义中的同名成员函数才存在重载关系。主要特点是函数名相同,参数类型和数目有所不同,不能出现参数个数和类型均相同,仅仅依靠返回值不同来区分的函数。重载和函数成员是否是虚函数无关。
2. 重写(覆盖)
重写指的是在派生类中覆盖基类中的同名函数,重写就是重写函数体,要求基类函数必须是虚函数且:
(1)与基类的虚函数有相同的参数个数
(2)与基类的虚函数有相同的参数类型
(3)与基类的虚函数有相同的返回值类型
3. 隐藏
隐藏指的是某些情况下,派生类中的函数屏蔽了基类中的同名函数,包括以下情况:
(1)两个函数参数相同,但是基类函数不是虚函数。隐藏和重写的区别在于基类函数是否是虚函数。
(2)两个函数参数不同,无论基类函数是不是虚函数,都会被隐藏。和重载的区别在于两个函数不在同一个类中。
七. C++有哪几种构造函数
1. 默认构造函数
以Student类为例,默认构造函数的原型为
Student(); //没有参数
默认构造函数和初始化构造函数在定义类的对象的时候,完成对象的初始化工作。
2. 初始化构造函数
以Student类为例,初始化构造函数的原型为
Student(int num,int age);//有参数
3. 拷贝(复制)构造函数
拷贝构造函数用于拷贝本类的对象,形参是本类对象的引用:
Student s2(1002,1008);
Student s3(s2);//将对象s2复制给s3。注意复制和赋值的概念不同。
//下面这种情况叫做赋值,不调用复制构造函数。
Student s4;
s4=s2;//这种情况叫做赋值
调用场景:
- 用类的一个实例化对象去初始化另一个对象的时候
- 函数的参数是类的对象时(非引用传递)
- 函数的返回值是函数体内局部对象的类的对象时 ,此时虽然发生(Named return Value优化)NRV优化,但是由于返回方式是值传递,所以会在返回值的地方调用拷贝构造函数
4. 转换构造函数
转换构造函数用于将其他类型的变量,隐式转换为本类对象:
Student(int r);//形参时其他类型变量,且只有一个形参
//转换构造函数
Student(int r)
{
int num=1004;
int age= r;
}
5. 移动构造函数
移动构造函数主要用对象a自身的空间初始化对象b,避免了新的空间的分配,降低了构造的成本。为了防止浅拷贝导致两个指针共同指向一片内存空间,将第一个指针(比如a->value)置为NULL,这样在调用析构函数的时候,由于有判断是否为NULL的语句,所以析构a的时候并不会回收a->value指向的空间;
八. 浅拷贝和深拷贝的区别
浅拷贝
浅拷贝只是拷贝一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,如果原来的指针所指向的资源释放了,那么再释放浅拷贝的指针的资源就会出现错误。
深拷贝
深拷贝不仅拷贝值,还开辟出一块新的空间用来存放新的值,即使原先的对象被析构掉,释放内存了也不会影响到深拷贝得到的值。在自己实现拷贝赋值的时候,如果有指针变量的话是需要自己实现深拷贝的。
区别:
浅拷贝在对象的拷贝创建时存在风险,即被拷贝的对象析构释放资源之后,拷贝对象析构时会再次释放一个已经释放的资源,深拷贝的结果是两个对象之间没有任何关系,各自成员地址不同。
九. 什么是内存泄露,如何检测和避免
内存泄露
一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定)内存块,使用完后必须显式释放的内存。应用程序般使用malloc,、realloc、 new等函数从堆中分配到块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。
避免内存泄露的几种方式
- 计数法:使用new或者malloc时,让该数+1,delete或free时,该数-1,程序执行完打印这个计数,如果不为0则表示存在内存泄露
- 一定要将基类的析构函数声明为虚函数
- 对象数组的释放一定要用delete []
- 有new就有delete,有malloc就有free,保证它们一定成对出现
十. 面向对象的三大特性
(1)继承
让某种类型对象获得另一个类型对象的属性和方法。它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
(2)封装
数据和代码捆绑在一起,避免外界干扰和不确定性访问。
封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏,例如:将公共的数据或方法使用public修饰,而不希望被访问的数据或方法采用private修饰。
(3)多态
同一事物表现出不同事物的能力,即向不同对象发送同一消息,不同的对象在接收时会产生不同的行为(重载实现编译时多态,虚函数实现运行时多态)
实现多态有二种方式:覆盖(override),重载(overload)。
覆盖:是指子类重新定义父类的虚函数的做法。
重载:是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。例如:基类是一个抽象对象——人,那教师、运动员也是人,而使用这个抽象对象既可以表示教师、也可以表示运动员。
十. C++的四种强制转换
1. static_cast
静态类型转换,对应于C语言中的隐式类型转换场景,可以转换基础数据类型,但是不能转换指针类型。该类型转换会在编译时进行类型检查。
用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换时:
进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;
进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的
2. reinterpret_cast
重新解释类型,可用于转换指针、引用、算术类型、函数指针或者成员指针。但是不能转换基础数据类型。
3. dynamic_cast
动态类型转换,会进行动态类型检查,应用场景是在多态场景中(子类对象传给父类指针或引用),可以动态检查子类的类型。
在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;
在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。
4. const_cast
应用场景是去除只读属性,但有一个前提是内存本身必须是可以修改的(内存分配在堆栈上)。
十一. 指针函数和函数指针的区别
1. 指针函数
它的本质是一个函数,不过它的返回值是一个指针。
# include <stdio.h>
# include <stdlib.h>
int * func_sum(int n)
{
if (n < 0)
{
printf("error:n must be > 0\n");
exit(-1);
}
static int sum = 0; //静态局部变量在整个程序运行期间存在,不加static会错
int *p = ∑
for (int i = 0; i < n; i++)
{
sum += i;
}
return p;
}
int main(void)
{
int num = 0;
printf("please input one number:");
scanf("%d", &num);
int *p = func_sum(num);
printf("sum:%d\n", *p);
return 0;
}
2. 函数指针
函数指针 的本质是一个指针,该指针的地址指向了一个函数,所以它是指向函数的指针。函数的定义是存在于代码段,因此,每个函数在代码段中,也有着自己的入口地址,函数指针就是指向代码段中函数入口地址的指针。
#include <stdio.h>
int max(int a, int b)
{
return a > b ? a : b;
}
int main(void)
{
int (*p)(int, int); //函数指针的定义
//int (*p)(); //函数指针的另一种定义方式,不过不建议使用
//int (*p)(int a, int b); //也可以使用这种方式定义函数指针
p = max; //函数指针初始化
int ret = p(10, 15); //函数指针的调用
//int ret = (*max)(10,15);
//int ret = (*p)(10,15);
//以上两种写法与第一种写法是等价的,不过建议使用第一种方式
printf("max = %d \n", ret);
return 0;
}
3. 函数指针在回调函数中的运用
回调函数就是一个通过指针函数调用的函数。其将函数指针作为一个参数,传递给另一个函数。回调函数并不是由实现方直接调用,而是在特定的事件或条件发生时由另外一方来调用的。
#include<stdio.h>
#include<stdlib.h>
//函数功能:实现累加求和
int func_sum(int n)
{
int sum = 0;
if (n < 0)
{
printf("n must be > 0\n");
exit(-1);
}
for (int i = 0; i < n; i++)
{
sum += i;
}
return sum;
}
//这个函数是回调函数,其中第二个参数为一个函数指针,通过该函数指针来调用求和函数,并把结果返回给主调函数
int callback(int n, int (*p)(int))
{
return p(n);
}
int main(void)
{
int n = 0;
printf("please input number:");
scanf("%d", &n);
printf("the sum from 0 to %d is %d\n", n, callback(n, func_sum)); //此处直接调用回调函数,而不是直接调用func_sum函数
return 0;
}
十二. C++的内存分区
C++中的内存分区,分别是堆、栈、自由存储区、全局/静态存储区、常量存储区和代码区。
栈:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
堆:就是那些由 new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个 delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
全局/静态存储区:全局变量、静态变量、常量、字符串常量被分配到同一块内存中,在以前的C语言中,全局变量和静态变量又分为初始化的和未初始化的,在C++里面没有这个区分了,它们共同占用同一块内存区,在该区定义的变量若没有初始化,则会被自动初始化,例如int型变量自动初始为0。
代码区:存放函数体的二进制代码。