C++知识点
关键字和库函数
字符串
三种方法保存字符串的方法:
- char *s1:s1是一个指向字符串的指针;
- char s2[]:s2是一个字符数组;
- string s3:s3是一个string类的对象;
字符串常量:
如"helloworld"
为一个字符串常量,其保存在常量区,只读,程序结束后由系统释放空间,类型为const char *
;
char * a=”string1”
已不再支持,因为const char*
与char *
类型不兼容;
概念辨析
-
char str[]="hello"
- 等价于
char str[]={'h', 'e', 'l', 'l', 'u','\0'};
- str为数组名,也是数组首地址,
*str
为h,*(str+5)
为'\0'
- str虽然是地址,但其并不是指针变量,
typeid(str).name()
为char[8]
- 类型
char[8],char*,const char*
均被printf("%s\n",str);
打印
- 等价于
-
string str="hello"
-
用字符串常量,拷贝构造了str对象,
char[8],char*,const char*
均可用于赋值或拷贝构造str对象 -
str.c_str()
或str.data()
方法返回const char*
类型的字符串 -
转换为
char *
char *data; int len = s1.length(); data = (char *)malloc((len + 1)*sizeof(char)); s1.copy(data, len, 0); data[len] = '\0';
-
转换为
char[8]
char buf[10]; strcpy(buf, str.c_str()); buf[str.length()] = '\0';
-
内存模型
char *s1 = “hello”;
char s2[] = “hello”;
内存模型如下
±----+ ±–±--±–±--±–±--+
s1: | *======> | h | e | l | l | o |\0 |
±----+ ±–±--±–±--±–±--+
±–±--±–±--±–±--+
s2: | h | e | l | l | o |\0 |
±–±--±–±--±–±--+
const
-
const
修饰变量/指针变量-
将其为
const
常量,使其值不能被改变,相比define还可以进行类型检查; -
const int *p
;int const *p
;const修饰 *p, *p不能被改变,即指针指向的值不能改变; -
int *const p
;const修饰指针变量p,即p的值不能被改变,指针的指向不能改变;
-
-
const
修饰函数返回值,使得函数调用表达式不能作为左值。 -
const
修饰函数形参,使传递过来的参数只有只读权限,如传引用并添加const修饰; -
const
修饰成员变量,成员变量只能在类内声明、定义,在构造函数初始化列表中初始化; -
const
修饰成员函数,写在函数后面int show() const{}
,使得该成员函数对成员变量只有只读权限,相当于修饰const this*
;
const与define的区别
define
是在编译预处理阶段进行替换,const
是在编译阶段确定其值。const
可以让编译器进行类型检查const
定义的常量占用静态存储区的空间,程序运行过程中只有一份。define在多处替换const
定义的常量可以进行调试。
static
- 修饰全局变量,或函数,使其仅在当前
.cpp
文件内有效,防止命名空间的污染 - 修饰局部变量,使其生命周期延长到程序结束。循环中static局部变量只会被声明和初始化一次。
- 修饰成员变量或成员函数,将其声明为属于该类的公有资源
- 静态成员函数不能声明成虚函数(
virtual
)、const
函数和volatile
函数。上述三个修饰的机制依赖对象的实例化,而static修饰类域公有资源,语义矛盾
- 静态成员函数不能声明成虚函数(
静态成员变量与静态成员函数
-
静态成员变量的作用域是属于整个类家族的,包括声明该静态成员变量的类,以及继承了这个类的类;
-
静态成员变量之只能类内声明,类外初始化;即声明和初始化不得不分开,无法在初始化列表对其初始化,但可在函数体内赋值;
-
静态成员不通过对象访问,可通过指定类作用域直接访问。当然也可以通过成员函数或静态成员函数访问;
-
私有静态成员由于权限问题,也无法指定类作用域直接访问,只能通过静态成员函数或成员函数访问;
class A{ private: static int my_svalA; public: static int svalA; void changeA(int value){my_svalA=value;} static void schangeA(int value){my_svalA=value;} }; int A::my_svalA=4396; //如不初始化,动态检查不到,但一旦编译时构造A类(A a或者遇到A::)则会报错 int A::svalA=4396; int main(){ A::svalA=666; A::schangeA(777); A a; a.changeA(888); a.schangeA(123);//编译器会警告,相当于A::schangeA(123); }
extern
- 置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。
extern “C"{}
:连接指定,是用C语言的编译规则。extern
和static
修饰全局变量或函数时具有相反的作用,是水火不容的。extern
声明的变量,要与其在另一个.cpp
中声明的格式完全相同。如char a[]不能对应 extern char *a;具有较强耦合性,当源定义修改,extern声明没有改变,则可能引发错误。因此一般使用#include头文件,而不使用extern
inline
- 修饰函数:使该函数在编译阶段展开为代码附加在程序段中,而不使用普通函数调用的跳转机制。
- 函数体积较小可以增加效率,一般函数体只有几行的时候使用。
- 类内定义的成员函数默认为
inline
函数,类外定义的函数则需要使用inline
显式声明。
运算符重载
设重载的运算符为$
- 若
operator$
重载为全局函数operator$(a,b)
,那么a$b
相当于operator$(a,b);
- 若
operator$
重载为成员函数operator$(b)
,那么a$b
相当于a.operator$(b);
- 返回值为引用类型时,才能实现链式编程;
- 左移或右移运算符不得不重载为全局函数,因为我们希望使用cout<<num,而非num<<cout;operator<<的两个参数:p代表传入对象,获取需要输出的值;cout代表向终端输出流; 如果作为成员函数,只能p.operator<<(cout),所以只能通过p<<cout调用只有重载为全局函数,且形参列表中ostream&在前,才能实现cout<<p的形式形参必须是ostream&,因为cout无法被拷贝,返回值实际上可以为其他,但返回ostream&,才能实现连用;友元声明为private或public没有区别。
- operator+可重载为成员函数或全局函数,认为规定其重载为成员函数
- 前置++可重载为成员函数或全局函数,若不返回Complex&,则++++num只会递增一次。
- 后置++可重载为成员函数或全局函数,使用const修饰返回值,使函数返回的对象只能作为左值,即num++++会编译报错;后置++的逻辑是对象自增,但返回自增前的值,通过构造一个临时对象实现。这就决定num++++只能自增一次,其后续的++自增的是临时对象,没有意义。后置++不能返回引用,返回临时对象的引用是错误的。
- 赋值运算符必须返回引用才能使链式编程有效,若不返会引用a=b=c报错,传入了一个临时对象的引用,(a=b)=c不报错,但a和b不会被c复制,c的值赋给了临时对象。
- 对函数调用运算符()的重载也叫仿函数。
#include<bits/stdc++.h>
using namespace std;
class Complex{
int a,b;
public:
Complex():a(0),b(0){};
Complex(int _a,int _b):a(_a),b(_b){};
friend ostream& operator<<(ostream& out, Complex& p);
friend ostream& operator<<(ostream& out, const Complex& p);
Complex operator+(const Complex &p2)const;
Complex& operator++();
const Complex operator++(int);
Complex& operator=(const Complex &p2);
};
Complex Complex:: operator+(const Complex &p2)const{
return {a+p2.a,b+p2.b};
}
ostream& operator<<(ostream& out, const Complex& p) {
out << p.a << "+" << p.b<<'i';
return out;
}
ostream& operator<<(ostream& out, Complex& p) {
out << p.a << "+" << p.b<<'i';
return out;
}
Complex& Complex:: operator++(){
a++;
return *this;
}
const Complex Complex::operator++(int){ //后置++写法特殊,且一般采用调用前置++实现
Complex temp=*this;
++*this;
return temp;
}
Complex& Complex::operator=(const Complex &p) //浅拷贝的=运算符重载是默认的,这里可以直接=default;
{
a=p.a;b=p.b;
return *this;
}
控制对象只能建立在栈区
//将new和delete运算符重载对A特化的,且设为私有,外部便无法使用了
class A {
private :
void * operator new ( size_t t){} // 注意函数的第一个参数和返回值都是固定的
void operator delete ( void * ptr){} // 重载了new就需要重载delete
public :
A(){}
~A(){}
};
多态
-
静态多态/编译期多态:
编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用那个函数,如果有对应的函数就调用该函数,否则出现编译错误。
- 函数重载:包括普通函数的重载、运算符重载和成员函数的重载,可以重用函数名,编译器根据函数参数类型推断调用的是具体哪个函数实现,并在编译阶段完成对函数地址的绑定。
- 函数模板:将类型作为参数,传递给模板,可使编译器生成该类型的函数。
-
动态多态/运行时多态:
在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。
- 条件:1.具有继承关系;2.派生类重写基类的虚函数;3.父类的指针或引用指向派生类的对象;
-
为什么要使用多态?
1.可以提高代码的复用性,应用程序不必为每一个派生类编写功能调用,只需要对抽象基类进行处理即可。
2.可以解决项目中紧偶合的问题,提高程序的可扩展性。派生类的功能可以被基类的方法或引用变量所调用,可以向后兼容,可以提高可扩充性和可维护性。
虚表指针、虚函数表、虚函数
-
如题,三者构成多态机制。当类中有虚函数或者其继承了具有虚函数的类时(无论自己有没有写虚函数),都会触发多态机制,即编译器在内存的模块区域为该类创建一个虚函数表。在该类对象实例化时,会隐形的包含一个虚表指针,指针的值为虚函数表的地址。
-
虚表指针:一个对象一个虚表指针,构造时初始化为虚表的地址。
VS所带编译器是把虚表指针放在了内存的开始处(0地址偏移),然后再是成员变量;在构造函数创建虚表指针以及虚表的。
-
虚函数表:一个类具有一个虚函数表,里面存放着一些虚函数的地址。子类虚函数表中的val(函数地址),会继承(copy)父类表中的值,除非子类重写父类的虚函数,相当于将表中的这条数据覆盖为新的。
虚函数表存放在全局区。
-
虚函数:虚函数与的实现存放在另一处,可被虚函数表索引到。一个虚函数可能被多个类检索到。
-
基类的指针或引用指向派生类的对象:
A *ptr=new B();
首先一个ptr指向B型对象b,那么ptr的值即对齐这个b的基地址,即ptr作用的一块区域内是B结构的一套东西。这个ptr是A类型的,只是限制了其寻址范围为A结构的形式。如A中2个int,B中继承了这两个int并新增一个int,那么ptr只能访问前两个int。A结构和B结构的内存中都有虚表指针(凡是多态类肯定有啊,而且是最先初始化的),因此ptr必然能寻址到B中的虚表指针,进而访问B的虚表找到B的虚函数。 -
子类的虚表指针可以理解为是对父类虚表指针的继承,只不过其在初始化时指向了子类的虚函数表。子类中定义新的虚函数,并不会增加虚表指针的个数,只是扩充或覆盖子类的虚函数表
-
多重继承:一个类继承两个父类,那么其中会包含两套完整父类的东西。自然包括两个虚表指针和两个虚表。
-
多重虚继承:虚函数表不会生成多个,但虚表指针会继承多个。
内存相关
C++ 内存分区
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3dkVWBf6-1633963291844)(D:\Users\tpytpytpy\Desktop\C++常考概念.assets\70-16339623821363.png)]
#include <iostream>
using namespace std;
/*
说明:C++ 中不再区分初始化和未初始化的全局变量、静态变量的存储区,如果非要区分下述程序标注在了括号中
*/
int g_var = 0; // g_var 在全局区(.data 段)
char *gp_var; // gp_var 在全局区(.bss 段)
int main()
{
int var; // var 在栈区
char *p_var; // p_var 在栈区
char arr[] = "abc"; // arr 为数组变量,存储在栈区;"abc"为字符串常量,存储在常量区
const char *p_var1 = "123456"; // p_var1 在栈区;"123456"为字符串常量,存储在常量区
static int s_var = 0; // s_var 为静态变量,存在静态存储区(.data 段)
p_var = (char *)malloc(10); // 分配得来的 10 个字节的区域在堆区
free(p_var);
return 0;
}
栈
存放函数的局部变量、函数参数、返回地址等,由编译器自动分配和释放。
控制对象只能建立在栈区
//将new和delete运算符重载对A特化的,且设为私有,外部便无法使用了
class A {
private :
void * operator new ( size_t t){} // 注意函数的第一个参数和返回值都是固定的
void operator delete ( void * ptr){} // 重载了new就需要重载delete
public :
A(){}
~A(){}
};
堆
动态申请的内存空间,就是由 malloc 分配的内存块,由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收。
控制对象只能建立在堆区
class A{
protected :
A(){} //只私有构造函数,无法使用new,因为operator ()分配内存时不能提供构造功能。
~A(){} //只要析构函数被保护,在栈区创建对象时就会被编译器检测到,无法被自动析构的对象便不能创建在栈区。但为了良好的可读性,将构造函数也被保护,这样创建和销毁对象都使用自定义的函数接口,而不是一个new一个destroy。
public :
static A* create() //只能通过提供的creat()接口获得堆区的对象
{ return new A(); }
void destory() //只能通过提供的destory()接口获得堆区的对象
{ delete this ; }
};
new 和 malloc
- malloc :成功申请到内存,返回指向该内存的指针;分配失败,返回 NULL 指针。
new :内存分配成功,返回该对象类型的指针;分配失败,抛出 bac_alloc 异常。 - malloc、free 是库函数,而new、delete 是关键字。
new 申请空间时,无需指定分配空间的大小,编译器会根据类型自行计算;malloc 在申请空间时,需要确定所申请空间的大小。 - 对于自定义的类型,new 首先调用 operator new() 函数申请空间(底层通过 malloc 实现),然后调用构造函数进行初始化,最后返回自定义类型的指针;delete 首先调用析构函数,然后调用 operator delete() 释放空间(底层通过 free 实现)。malloc、free 无法进行自定义类型的对象的构造和析构。
new
操作符从自由存储区上为对象动态分配内存,而malloc
函数从堆上动态分配内存。(自由存储区不等于堆)
malloc 的原理
- 当开辟的空间小于 128K 时,调用 brk() 函数,通过移动 _enddata 来实现;
- 当开辟空间大于 128K 时,调用 mmap() 函数,通过在虚拟地址空间中开辟一块内存空间来实现。
- brk() 函数实现原理:向高地址的方向移动指向数据段的高地址的指针 _enddata。
- mmap 内存映射原理:进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域;调用内核空间的系统调用函数 mmap(),实现文件物理地址和进程虚拟地址的一一映射关系;进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝。
全局区/静态存储区(.bss 段和 .data 段)
存放全局变量和静态变量,程序运行结束操作系统自动释放,在 C 语言中,未初始化的放在 .bss 段中,初始化的放在 .data 段中,C++ 中不再区分了。
怎么在main函数执行之前执行一段代码
- C++程序先创建全局变量,然后在执行main函数
- 全局变量声明时初始化,并调用函数返回值
int g_iValue=func();
- 全局变量是类类型时,会先执行类的构造函数,因此在main前,全局变量的构造函数内的代码会被先执行
- 类的静态成员在
mian
函数前初始化。饿汉式单例模式。
常量存储区(.data 段)
存放的是常量,不允许修改,程序运行结束自动释放。
字符串常量”hello"存放在常量区
代码区(.text 段)
存放代码,不允许修改,但可以执行。编译后的二进制文件存放在这里。