C/C++知识点整理
C++关键词
const
- 指向常量的指针
int const *p;
const int *p; - 指针本身是常量
int *const p;
分辨:从右往左看,看const离谁近 - const修饰成员函数
void func() const{} 常成员函数,可以使用类中的所有成员变量,但是不能修改它们,一般用于返回成员变量;
define(预处理阶段)
define 在预处理阶段进行替换
define函数
1
define add(a,b) {a++; b++; cout<<a+b<<endl;}
inline(编译阶段)
内联函数和普通函数的区别:当编译器处理调用内联函数的语句时,不会将该语句编译成函数调用的指令,而是直接将整个函数体的代码插人调用语句处,就像整个函数体在调用处被重写了一遍一样。
- 优点: 有了内联函数,就能像调用一个函数那样方便地重复使用一段代码,而不需要付出执行函数调用的额外开销。
- 缺点:
- 使用内联函数会使最终可执行程序的体积增加。以空间换时间,或增加空间消耗来节省时间,这是计算机学科中常用的方法;
- inline造成代码膨胀会导致额外的换页行为,降低指令高速缓存装置的命中率,以及伴随这些而来的效率降低。——-摘自effective
内联函数中的代码应该只是很简单、执行很快的几条语句。如果一个函数较为复杂,它执行的时间可能上万倍于函数调用的额外开销,那么将其作为内联函数处理的结果是付出让代码体积增加不少的代价,却只使速度提高了万分之一,这显然是不划算的。
内联函数和宏的区别:宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的,而且内联函数是真正的函数。
Q: 类中的构造函数和析构函数可以是内联的吗?
A: 你当然可以将其声明为内联的,因为你有权利向编译器建议任何函数为内联的。哈哈,当然我们应该换一种问法,类中的构造函数和析构函数适合作为内联函数吗?effective里面说,构造函数和析构函数往往是inlining的糟糕候选人。因为编译器在编译期间会给你的构造函数和析构函数额外加入很多的代码,像成员函数的构造析构等代码,所以通常构造析构函数比表面上看起来的要多,并不适合作为内联函数。
static
全局静态变量
在全局变量前加上关键字static,全局变量就定义成一个全局静态变量。
静态存储区,在整个程序运行期间一直存在。
初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);
作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。局部静态变量
在局部变量之前加上关键字static,局部变量就成为一个局部静态变量。
内存中的位置:静态存储区
初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);
作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;静态函数
在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。
函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突;
warning:不要再头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加上static修饰;类的静态成员
static修饰的变量先于对象存在,所以static修饰的变量要在类外初始化。因为static是所有对象共享的东西嘛,必须要比对象先存在的。初始化不受private和protected访问限制,但是若是private,下面main函数就无法访问。1
2
3
4
5
6
7
8
9
10class Test{
private:
public:
static int i; //声明
}
int Test::i = 100;
int main():{
cout<<Test::i<<endl;
return 0;
}通常,非static数据成员存在于类类型的每个对象中。然而,static数据成员独立于该类的任意对象而存在;每个static数据成员是与类关联的对象,而不是与该类的对象相关联。
static数据成员定义:- 一般情况下,static数据成员是类内声明,类外定义;
- static成员不通过类构造函数初始化,而是在定义时进行初始化;
- 一个例外:初始化式为常量表达式,整型static const 数据成员(static const int) 可以在类的定义体内进行初始化:
类的静态函数
静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。
在静态成员函数的实现中不能直接引用类中说明的非静态成员(没有this指针),可以引用类中说明的静态成员(这点非常重要)。如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>);
注意:类的非static成员函数是可以直接访问类的static和非static成员,而不用作用域操作符。
使用static成员的优点:- 避免命名冲突:static成员的名字在类的作用域中,因此可以避免与其他类的成员或全局对象名字冲突。
- 可以实施封装:static成员可以是私有成员,而全局对象不可以。
- 易读性:static成员是与特定类关联的,可显示程序员的意图。
extern
extern有两个作用:
- extern “C”:当它与”C”一起连用时,如: extern “C” void fun(int a, int b);则告诉编译器在编译fun这个函数名时按着C的规则去翻译相应的函数名而不是C++的,C++的规则在翻译这个函数名时会把fun这个名字变得面目全非,可能是fun@aBc_int_int#%$也可能是别的,这要看编译器的”脾气”了(不同的编译器采用的方法不一样)
- extern:当extern不与”C”在一起修饰变量或函数时,如在头文件中: extern int g_Int; 它的作用就是声明函数或全局变量的作用范围的关键字,其声明的函数和变量可以在本模块或其他模块中使用,记住它是一个声明不是定义!也就是说B模块(编译单元)要是引用模块(编译单元)A中定义的全局变量或函数时,它只要包含A模块的头文件即可,在编译阶段,模块B虽然找不到该函数或变量,但它不会报错,它会在连接时从模块A生成的目标代码中找到此函数。
volatile
定义为volatile的变量是说这变量可能会被意想不到地改变,即在你程序运行过程中一直会变,你希望这个值被正确的处理,每次从内存中去读这个值,而不是因编译器优化从缓存的地方读取,比如读取缓存在寄存器中的数值,从而保证volatile变量被正确的读取。
C++模板以及底层实现
- 编译器并不是把函数模板处理成能够处理任意类的函数;编译器从函数模板通过具体类型产生不同的函数;编译器会对函数模板进行两次编译:在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。
- 这是因为函数模板要被实例化后才能成为真正的函数,在使用函数模板的源文件中包含函数模板的头文件,如果该头文件中只有声明,没有定义,那编译器无法实例化该模板,最终导致链接错误。
include “”和include<>区别
- include “”查找头文件路径顺序
当前头文件目录(比如D:\MyProjects\tmp\ )
编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)(比如C:\Keil\c51\INC\ )
系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径 - include<>查找头文件的路径顺序为:
编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)
系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径
指针和引用的区别
- 指针有自己的一块空间,而引用只是一个别名;
- 使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;
- 指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象的引用;
- 作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;
- 指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变;
- 指针可以有多级指针(**p),而引用只有一级;
- 指针和引用使用++运算符的意义不一样(一个指针本身的大小是4字节);
1
2
3
4
5
6
7
8
9int* p1,double* p2;
int a=1,double b=2;
p1=&a,p2=&b;
cout<<p1<<" "<<p2<<endl;
p1++; p2++;
//指向的内存空间+1,所以和类型有关,第一个+4,第二个+8
cout<<p1<<" "<<p2<<endl;
//本身内存空间+1,所以两个地址值都是+4
cout<<(&p1+1)<<" "<<(&p2+1);
调用赋值构造函数还是拷贝(复制)构造函数的情况(历史悠久,还没理清)
总结,还没初始化的调用拷贝构造函数,已经初始化过的调用赋值构造函数。
虚函数
1 | virtual void funtion() {函数体} |
虚函数,在基类声明,在其派生类中根据需要进行重写(不需要可以不重写,继承基类的虚函数)。
而声明纯虚函数的类为抽象类,不可定义对象,由其派生的子类想要使用该虚函数,必须重新实现该虚函数,不会自动继承抽象类的虚函数。有纯虚函数的类为抽象类,不能定义抽象类的对象,它的子类要么实现它所有的纯虚函数变为一个普通类,要么还是一个抽象类。
虚函数实现
虚函数表:类的虚函数表是一块连续的内存,每个内存单元中记录一个JMP指令的地址。
编译器会为每个有虚函数的类创建一个虚函数表,该虚函数表将被该类的所有对象共享。类的每个虚函数占据虚函数表中的一块。如果类中有N个虚函数,那么其虚函数表将有N*4字节的大小。
虚函数表是在编译阶段创建,运行时动态绑定虚函数
编译器另外还为每个特定类的对象提供了一个虚表指针(即vptr),这个指针指向了对象所属类的虚表。在程序运行时,根据对象的类型去初始化vptr,从而让vptr正确的指向所属类的虚表,从而在调用虚函数时,就能够找到正确的函数。
- 一个类中的某个方法被声明为虚函数,则它将放在虚函数表中。
- 当一个类继承了另一个类,就会继承它的虚函数表,虚函数表中所包含的函数,如果在子类中有重写,则指向当前重写的实现,否则指向基类实现。若在子类中定义了新的虚函数,则该虚函数指针在虚函数表的后面。
- 在继承或多级继承中,要用一个祖先类的指针调用一个后代类实例的方法,若想体现出多态,则必须在该祖先类中就将需要的方法声明为虚函数,否则虽然后代类的虚函数表中有这个方法在后代类中的实现,但对祖先类指针的方法调用依然是早绑定的。
有虚函数或虚继承的类实例化后的对象大小至少为4字节(确切的说是一个指针的字节数;说至少是因为还要加上其他非静态数据成员,还要考虑对齐问题);没有虚函数和虚继承的类实例化后的对象大小至少为1字节(没有非静态数据成员的情况下也要有1个字节来记录它的地址)。
构造函数能否使用虚函数,析构函数呢
- 构造函数不能为虚函数:构造函数在进行调用时还不存在父类和子类的概念,父类只会调用父类的构造函数,子类调用子类的,因此不存在动态绑定的概念;但是构造函数中可以调用虚函数,不过并没有动态效果,只会调用本类中的对应函数;
- 析构函数可以为虚函数:对象已经创建,虚表指针存放析构函数的地址,基类与派生类都含有析构虚函数,创建基类与子类对象,都含有各类的虚表指针,当写通用函数时,运行根据传入对象的类型确定析构函数的地址,然后调用该析构函数。
析构函数可以是纯虚函数,但是必须提供纯虚析构函数的定义。这个定义是必需的,因为虚析构函数工作的方式是:最底层的派生类的析构函数最先被调用,然后各个基类的析构函数被调用。这就是说,即使是抽象类,编译器也要产生对~awov的调用,所以要保证为它提供函数体。如果不这么做,链接器就会检测出来,最后还是得回去把它添上.1
2
3
4
5class awov {
public:
virtual ~awov() = 0; // 声明一个纯虚析构函数
};
awov::~awov() {} // 纯虚析构函数的定义