1.C/C++关键字
1.1 static(静态)变量
在C中,关键字static是静态变量:
- 静态变量只会初始化一次,然后在这函数被调用过程中值不变。
- 在文件内定义静态变量(函数外),作用域是当前文件,该变量可以被文件内所有函数访问,不能被其他文件函数访问。为本地的全局变量,只初始化一次。
在C++中,类内数据成员可以定义为static
- 对于非静态数据成员,每个对象有一个副本。而静态数据成员是类的成员,只存在一个副本,被所有对象共享。
- 静态成员变量没有实例化对象也可以使用,“类名:静态成员变量”
- 静态成员变量初始化在类外,但是private和protected修饰的静态成员不能类外访问。
class Stu
{
public:
static int age;
private:
static int height;
};
//初始化静态成员变量
int Stu::age = 19;
int Stu::height = 180;
int main()
{
cout<<Stu::age<<endl;//输出19;
cout<<Stu::height<<endl;//错误的,私有无法访问。
Stu s;
cout<<s::age<<endl;//输出19;
cout<<s::height<<endl;//错误的,私有无法访问。
return 0;
}
- 在类中,static修饰的函数是静态成员函数。静态成员函数一样属于类,不属于对象,被对象共享。静态成员函数没有this指针,不能访问非静态的函数和变量,只能访问静态的。
与全局变量相比,静态数据成员的优势:
- 全局变量作用域是整个工程,而static作用域是当前文件,避免命名冲突
- 静态数据成员可以是private成员,而全局变量不能,实现信息隐藏
为什么静态成员变量不能在类内初始化?
因为类的声明可能会在多处引用,每次引用都会初始化一次,分配一次空间。这和静态变量只能初始化一次,只有一个副本冲突,因此静态成员变量只能类外初始化。
为什么static静态变量只能初始化一次?
所有变量都只初始化一次。但是静态变量在全局区(静态区),而自动变量在栈区。静态变量生命周期和程序一样,只创建初始化一次就一直存在,不会销毁。而自动变量生命周期和函数一样,函数调用就进行创建初始化,函数结束就销毁,所以每一次调用函数就初始化一次。
在头文件中定义静态变量是否可行?
不可行,在头文件中定义的一个static变量,对于包含该头文件的所有源文件,实质上在每个源文件内定义了一个同名的static变量。造成资源浪费,可能引起bug
静态变量什么时候初始化
-
初始化只有一次,但是可以多次赋值,在主程序之前,编译器已经为其分配好了内存。
-
静态局部变量和全局变量一样,数据都存放在全局区域,所以在主程序之前,编译器已经为其分配好了内存,但在C和C++中静态局部变量的初始化节点又有点不太一样。
-
在C中,初始化发生在代码执行之前,编译阶段分配好内存之后,就会进行初始化,所以我们看到在C语言中无法使用变量对静态局部变量进行初始化,在程序运行结束,变量所处的全局内存会被全部回收。
-
而在C++中,初始化时在执行相关代码时才会进行初始化,C++标准定为全局或静态对象是有首次用到时才会进行构造,并通过atexit()来管理。在程序结束,按照构造顺序反方向进行逐个析构。所以在C++中是可以使用变量对静态局部变量进行初始化的。
1.2 const的作用
常量类型也称为const类型,使用const修饰变量或者对象
在C中,const的作用为:
- 定义变量(局部或者全局)为常量
const int a = 10; //常量定义时,必须初始化
- 修饰函数的参数,函数体内不能修改这个参数的值
- 修饰函数的返回值
- const修饰的返回值类型为指针,返回的指针不能被修改,而且只能符给被const修饰的指针
const char* GetString() { //... } int main() { char *str = GetString();//错误,str没被const修饰 const char *str = GetString();//正确 }
- const修饰的返回值类型为引用,那么函数调用表达式不能做左值(函数不能被赋值)
const int & add(int &a , int &b) { //.. } int main() { add(a,b) = 4;//错误,const修饰add的返回引用,不能做左值 }
- const修饰的返回值类型为普通变量,由于返回是普通临时变量,const修饰没意义。
- const修饰的返回值类型为指针,返回的指针不能被修改,而且只能符给被const修饰的指针
在c++中,const还有作用为:
- const修饰类内的数据成员。表示这个数据成员在某个对象的生命周期是常量,不同对象的值可以不一样,因此const成员函数不能在类内初始化。
- const修饰类内的成员函数。那么这个函数就不能修改对象的成员变量
const的优点?
-
进行类型检查,使编译器对处理内容有更多了解。
-
避免意义模糊的数字出现,类似宏定义,方便对参数进行修改。
-
保护被修饰的内容,防止被意外修改
-
为函数重载提供参考
class A { void f(int i){...} //非const对象调用 void f(int i) const {...}//const对象调用 }
5.节省内存
6.提高程序效率(编译器不为普通const常量分配存储空间,而保存在符号表中。称为一个编译期间的常量,没有存储和读内存的操作)
什么时候使用const?
-
修饰一般常量
-
修饰对象
-
修饰常指针
const int *p; int const *p; int *const p; const int *const p;
-
修饰常引用
-
修饰函数的参数
-
修饰函数返回值
-
修饰类的成员函数
-
修饰另一文件中引用的变量
extern const int j;
const和指针(常量指针、指针常量)
-
常量指针(const 修饰常量,const在*的左边)
const int *p = &a; // const修饰int,指针的指向可以修改,但是指针指向的值不能改 int const *p;//同上 p = &b;//正确 *p = 10;//错误
-
指针常量(const修饰指针,const在*的右边)
int *const p = &a;//const修饰指针,指针的指向不可以改,但是指针指向的值可以改 *p = 10;//正确 p = &b;//错误
-
const都修饰指针和常量(指针和常量都不能修改)
const int *const p; int const *const p;
顶层const和底层const
- 顶层const(常量指针):指的是const修饰的变量本身是一个常量,无法修改,指的是指针,就是 * 号的右边
- 底层const(指针常量):指的是const修饰的变量所指向的对象是一个常量,指的是所指变量,就是 * 号的左边
const和static的作用
static
- 不考虑类的情况
- 隐藏。所有不加static的全局变量和函数具有全局可见性,可以在其他文件中使用,加了之后只能在该文件所在的编译模块中使用
- 默认初始化为0,包括未初始化的全局静态变量与局部静态变量,都存在全局未初始化区
- 静态变量在函数内定义,始终存在,且只进行一次初始化,具有记忆性,其作用范围与局部变量相同,函数退出后仍然存在,但不能使用
- 考虑类的情况
- static成员变量:只与类关联,不与类的对象关联。定义时要分配空间,不能在类声明中初始化,必须在类定义体外部初始化,初始化时不需要标示为static;可以被非static成员函数任意访问。
- static成员函数:不具有this指针,无法访问类对象的非static成员变量和非static成员函数;不能被声明为const、虚函数和volatile;可以被非static成员函数任意访问
const
-
不考虑类的情况
-
const常量在定义时必须初始化,之后无法更改
-
const形参可以接收const和非const类型的实参,例如// i 可以是 int 型或者 const int 型void fun(const int& i){ //…}
-
-
考虑类的情况
- const成员变量:不能在类定义外部初始化,只能通过构造函数初始化列表进行初始化,并且必须有构造函数;不同类对其const数据成员的值可以不同,所以不能在类中声明时初始化
- const成员函数:const对象不可以调用非const成员函数;非const对象都可以调用;不可以改变非mutable(用该关键字声明的变量可以在const成员函数中被修改)数据的值
补充一点const相关:const修饰变量是也与static有一样的隐藏作用。只能在该文件中使用,其他文件不可以引用声明使用。 因此在头文件中声明const变量是没问题的,因为即使被多个文件包含,链接性都是内部的,不会出现符号冲突
1.3 switch语句中case结尾是否必须加break
**一般必须在case结尾加break。**因为通过switch确认入口点,一直往下执行,直到遇见break。否则会执行完这个case后执行后面的case,default也会执行。 注,switch(c),c可以是int、long、char等,但是不能是float
1.4 volatile 的作用
volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。
- 编译器不再进行优化,从而可以提供对特殊地址的稳定访问。
- 系统总是重新从它所在的内存读取数据,不会利用cache中原有的数值。
- 用于多线程被多个任务共享的变量,或者并行设备的硬件寄存器
1.5 断言ASSERT()是什么?
**是一个调试程序使用的宏。**定义在<assert.h>中,用于判断是否出现非法数据。括号内的值 为false(0),程序报错,终止运行。
ASSERT(n != 0);// n为0的时候程序报错
k = 10/n;
ASSERT()在Debug中有,在Release中被忽略。 ASSERT()是宏,assert()是ANSCI标准中的函数,但是影响程序性能。
1.6 枚举变量的值计算
#include<stdio.h>
int main()
{
enum {a,b=5,c,d=4,e};
printf("%d %d %d %d %d",a,b,c,d,e);
return 0;
}
输出为 0 5 6 4 5
1.7 字符串存储方式
- 字符串存储在栈中
char str1[] = "abc";
char str2[] = "abc";
- 字符串存储在常量区
char *str3 = "abc";
char *str4 = "abc";
- 字符串存储在堆中
char *str5 = (char*)malloc(4);
strcpy(str5,"abc");
char *str6 = (char*)malloc(4);
strcpy(str6,"abc");
- 字符串是否相等
- str1 != str2 ,str1和str2是两个字符串的首地址。
- str3 == srt4 , str3和str4是常量的地址,同样字符串在常量区只存在一份。
- str5 != str6 ,str5 和str6是指向堆的地址。
1.8 程序内存分区
内存高地址 | 栈区 |
---|---|
堆区 | |
全局/静态区 (.bss段 .date段) | |
常量区 | |
内存低地址 | 代码区 |
- 栈区(stack)
-
临时创建的局部变量存放在栈区。
-
函数调用时,其入口参数存放在栈区。
-
函数返回时,其返回值存放在栈区。
-
const定义的局部变量存放在栈区。
- 堆区(heap)
-
堆区用于存放程序运行中被动态分布的内存段,可增可减。
-
malloc函数分布的内存,必须用free进行内存释放,否则会造成内存泄漏。
- 全局区(静态区)
- (c语言中)全局区有.bss段和.data段组成,可读可写。
- C++不分bss和data
- .bss段
-
未初始化的全局变量存放在.bss段。
-
初始化为0的全局变量和初始化为0的静态变量存放在.bss段。
-
.bss段不占用可执行文件空间,其内容有操作系统初始化。
- .data段
-
已经初始化的全局变量存放在.data段。
-
静态变量存放在.data段。
-
.data段占用可执行文件空间,其内容有程序初始化。
-
const定义的全局变量存放在.rodata段。
- 常量区
-
字符串存放在常量区。
-
常量区的内容不可以被修改。
- 代码区
- 程序执行代码(二进制代码文件)存放在代码区。
1.9 *p++ 和 (*p)++ 的区别
- *p++ 先完成取地址,然后对指针地址进行++,再取值
- (*p)++,先完成取值,再对值进行++
1.10 new / delete 与 malloc / free的异同
-
相同点
- 都可用于内存的动态申请和释放
-
不同点
-
new / delete 是C++运算符,malloc / free是C/C++语言标准库函数
-
new自动计算要分配的空间大小,malloc需要手工计算
-
malloc和free返回的是void类型指针(必须进行类型转换),new和delete返回的是具体类型指针。
-
new是类型安全的,malloc不是。例如:
int *p = new float[2]; //编译错误 int *p = (int*)malloc(2 * sizeof(double));//编译无错误
-
malloc / free需要库文件支持,new / delete不用
-
new是封装了malloc,直接free不会报错,但是这只是释放内存,而不会析构对象
-
1.11 new和delete是如何实现的?
- new的实现过程是:首先调用名为operator new的标准库函数,分配足够大的原始为类型化的内存,以保存指定类型的一个对象;接下来运行该类型的一个构造函数,用指定初始化构造对象;最后返回指向新分配并构造后的的对象的指针
- delete的实现过程:对指针指向的对象运行适当的析构函数;然后通过调用名为operator delete的标准库函数释放该对象所用内存
1.12 被free回收的内存是立即返还给操作系统吗?
不是的,被free回收的内存会首先被ptmalloc使用双链表保存起来,当用户下一次申请内存的时候,会尝试从这些内存中寻找合适的返回。这样就避免了频繁的系统调用,占用过多的系统资源。同时ptmalloc也会尝试对小块内存进行合并,避免过多的内存碎片。
1.13 C++中几种类型的new
-
plain new
言下之意就是普通的new,就是我们常用的new,在C++中定义如下:void* operator new(std::size_t) throw(std::bad_alloc); void operator delete(void *) throw();
plain new在空间分配失败的情况下,抛出异常std::bad_alloc而不是返回NULL
#include <iostream> #include <string> using namespace std; int main() { try { char *p = new char[10e11]; delete p; } catch (const std::bad_alloc &ex) { cout << ex.what() << endl; } return 0; } //执行结果:bad allocation
-
nothrow new
nothrow new在空间分配失败的情况下是不抛出异常,而是返回NULL,定义如下:void * operator new(std::size_t,const std::nothrow_t&) throw(); void operator delete(void*) throw();
#include <iostream> #include <string> using namespace std; int main() { char *p = new(nothrow) char[10e11]; if (p == NULL) { cout << "alloc failed" << endl; } delete p; return 0; } //运行结果:alloc failed
-
placement new
这种new允许在一块已经分配成功的内存上重新构造对象或对象数组。placement new不用担心内存分配失败,因为它根本不分配内存,它做的唯一一件事情就是调用对象的构造函数。定义如下:void* operator new(size_t,void*); void operator delete(void*,void*);
使用placement new需要注意两点:
-
palcement new的主要用途就是反复使用一块较大的动态分配的内存来构造不同类型的对象或者他们的数组
-
placement new构造起来的对象数组,要显式的调用他们的析构函数来销毁(析构函数并不释放对象的内存),千万不要使用delete,这是因为placement new构造起来的对象或数组大小并不一定等于原来分配的内存大小,使用delete会造成内存泄漏或者之后释放内存时出现运行时错误。
#include <iostream> #include <string> using namespace std; class ADT{ int i; int j; public: ADT(){ i = 10; j = 100; cout << "ADT construct i=" << i << "j="<<j <<endl; } ~ADT(){ cout << "ADT destruct" << endl; } }; int main() { char *p = new(nothrow) char[sizeof ADT + 1]; if (p == NULL) { cout << "alloc failed" << endl; } ADT *q = new(p) ADT; //placement new:不必担心失败,只要p所指对象的的空间足够ADT创建即可 //delete q;//错误!不能在此处调用delete q; q->ADT::~ADT();//显示调用析构函数 delete[] p; return 0; } //输出结果: //ADT construct i=10j=100 //ADT destruct
1.14 delete p、delete [] p、allocator都有什么作用?
-
delete p ,为消除一个对象。
-
delete[]时,数组中的元素按逆序的顺序进行销毁;
-
new在内存分配上面有一些局限性,new的机制是将内存分配和对象构造组合在一起,同样的,delete也是将对象析构和内存释放组合在一起的。allocator将这两部分分开进行,allocator申请一部分内存,不进行初始化对象,只有当需要的时候才进行初始化操作。
1.15 malloc与free的实现原理?
1、 在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk、mmap、munmap这些系统调用实现的;
2、 brk是将数据段(.data)的最高地址指针_edata往高地址推,mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系;
3、 malloc小于128k的内存,使用brk分配内存,将_edata往高地址推;malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配;brk分配的内存需要等到高地址内存释放以后才能释放,而mmap分配的内存可以单独释放。当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。在上一个步骤free的时候,发现最高地址空闲内存超过128K,于是内存紧缩。
4、 malloc是从堆里面申请内存,也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。
1.16 malloc、realloc、calloc的区别
-
malloc函数
void* malloc(unsigned int num_size); int *p = malloc(20*sizeof(int));申请20个int类型的空间;
-
calloc函数
void* calloc(size_t n,size_t size); int *p = calloc(20, sizeof(int));
省去了人为空间计算;malloc申请的空间的值是随机初始化的,calloc申请的空间的值是初始化为0的;
-
realloc函数
void realloc(void *p, size_t new_size);
给动态分配的空间分配额外的空间,用于扩充容量。
1.17 exit()和return 的区别
- return是语言级的,标志调用堆栈的返回。是从当前函数的返回,main()中return的退出程序
- exit()是函数,强行退出程序,并返回值给系统
- return实现函数逻辑,函数的输出。exit()只用来退出。
1.18 extern和export的作用
变量的声明有两种情况:
-
一种是需要建立存储空间的。例如:int a 在定义的时候就已经建立了存储空间。
-
另一种是不需要建立存储空间的。 例如:extern int a 其中变量a是在别的文件中定义的。
-
总之就是:把建立空间的声明成为“定义”,把不需要建立存储空间的成为“声明”。
- extern
- 普通变量、类。结构体
- export(C++中新增)
- 和exturn类似,但是用作模板
- 使用该关键字可实现模板函数的外部调用
- 模板实现的时候前面加上export,别的文件包含头文件就可用该模板
extern"C"的用法
在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern "C"声明,在.c文件中包含了extern "C"时会出现编译语法错误。**所以使用extern "C"全部都放在于cpp程序相关文件或其头文件中。
C++中调用C代码:
//xx.h
extern int add(...)
//xx.c
int add(){
}
//xx.cpp
extern "C" {
#include "xx.h"
}
C调用C++函数
//xx.h
extern "C"{
int add();
}
//xx.cpp
int add(){
}
//xx.c
extern int add();
1.19 C++中,explicit的作用
explicit阻止隐式转换
-
隐式转换
String s1 = "hello"; //进行隐式转换,等价于 String s1 = String("hello");
-
explicit阻止隐式转换
class Test1 { public: Test1(int n){ num = n } private: int num; } class Test2 { public: explicit Test2(int n){ num = n } private: int num; } int main() { Test1 t1 = 1; //正确,隐式转换 Test2 t2 = 1;//错误,禁止隐式转换 Test2 t2(1); //正确,可与显示调用 }
1.20 C++的异常处理
C++中的异常处理机制主要使用try、throw和catch三个关键字
#include <iostream>
using namespace std;
int main()
{
double m = 1, n = 0;
try {
cout << "before dividing." << endl;
if (n == 0)
throw - 1; //抛出int型异常
else if (m == 0)
throw - 1.0; //拋出 double 型异常
else
cout << m / n << endl;
cout << "after dividing." << endl;
}
catch (double d) {
cout << "catch (double)" << d << endl;
}
catch (...) {
cout << "catch (...)" << endl;
}
cout << "finished" << endl;
return 0;
}
//运行结果
//before dividing.
//catch (...)
//finished
代码中,对两个数进行除法计算,其中除数为0。可以看到以上三个关键字,
- 程序的执行流程是先执行try包裹的语句块,如果执行过程中没有异常发生,则不会进入任何catch包裹的语句块,如果发生异常,则使用throw进行异常抛出,再由catch进行捕获,
- throw可以抛出各种数据类型的信息,代码中使用的是数字,也可以自定义异常class。catch根据throw抛出的数据类型进行精确捕获(不会出现类型转换),如果匹配不到就直接报错,可以使用catch(…)的方式捕获任何异常(不推荐)。
- 当然,如果catch了异常,当前函数如果不进行处理,或者已经处理了想通知上一层的调用者,可以在catch里面再throw异常。
1.21 回调函数
-
把一段可执行的代码像参数传递那样传给其他代码,而这段代码会在某个时刻被调用执行,这就叫做回调。
-
如果代码立即被执行就称为同步回调,如果过后再执行,则称之为异步回调。
-
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
-
主函数和回调函数是在同一层的,而库函数在另外一层。如果库函数对我们不可见,我们修改不了库函数的实现,也就是说不能通过修改库函数让库函数调用普通函数那样实现,那我们就只能通过传入不同的回调函数
-
sort(),中自定义的cmp就是回调函数
1.22 C++中,mutable的作用
mutable的中文意思是“可变的,易变的”,在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。
class person
{
int m_A;
mutable int m_B;//特殊变量 在常函数里值也可以被修改
public:
void add() const//在函数里不可修改this指针指向的值 常量指针
{
m_A=10;//错误 不可修改值,this已经被修饰为常量指针
m_B=20;//正确
}
}
int main()
{
const person p;//修饰常对象 不可修改类成员的值
p.m_A=10;//错误,被修饰了指针常量
p.m_B=200;//正确,特殊变量,修饰了mutable
}
2. 内存分配
2.1 C++内存分配
见 1.8
2.2 内存泄漏
内存泄露的原因
内存泄漏是指堆内存的泄漏。使用malloc,、realloc、 new等函数从堆中分配到块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,如果没有释放内存这块内存就不能被再次使用,我们就说这块内存泄漏了
避免内存泄露的几种方式
- 计数法:使用new或者malloc时,让该数+1,delete或free时,该数-1,程序执行完打印这个计数,如果不为0则表示存在内存泄露
- 一定要将基类的析构函数声明为虚函数(这样子类的析构函数必须重新实现,避免忘记释放内存)
- 对象数组的释放一定要用delete []
- 有new就有delete,有malloc就有free,保证它们一定成对出现
内存泄漏检测工具
- 从Linux下可以使用Valgrind工具
- Windows下可以使用CRT库
2.3 栈默认的大小
- Windows 下是2MB
- Linux下是8MB(ulimit-s 设置)
2.4 sizeof() 和 strlen()的区别
int a ,b;
a = strlen("