C++基础知识
文章目录
C++ 中,四个与类型转换相关的关键字?
const_cast、static_cast、dynamic_cast、reinterpret_cast
-
const_cast
强制去掉const或 volatile(多线程使用)
-
static_cast 静态转换 (类型相关)
运算符完成C++内置基本数据相关类型之间的转换。默认整数和char之间。
-
reinterpret_cast (类型不相关)
处理互不相关类型之间的转换,从整型到指针,从一种类型的指针到另一种类型。
int指针到char指针
-
dynamic_cast 动态转换
处理基类到派生类型的转换(下行),给了NXobject,转到line,body等
也可以在类层次间进行上行转换,从派生类转为基类,此时,dynamic_cast和static_cast相同。
从基类指针获得派生类行为最好的方法是通过虚函数。当使用虚函数的时候,编译器自动根据对象的实际类型选择正确的函数。但是,在某些情况下,不可能使用虚函数。这时候就需要使用dynamic_cast关键字了。但是,能用虚函数还是用虚函数最好。
与其他强制类型转换不同,dynamic_cast涉及运行时类型检查。如果绑定到引用或指针的对象不是目标类型的对象,则dynamic_cast失败。
补充:
在上面四个类型转化关键字中,除了static_cast,其他的三个都有可能涉及到指针的类型转换。从本质上来说,指针的类型不同,并没有产生很大的差异,他们都是需要足够的内存来存放一个机器地址。“指向不同类型之各指针”间的差异,既不在其指针表示法不同,也不在其内容(代表一个地址)不同,而是在其所寻址出来的object不同。也就是说,“指针类型”会教导编译器如何解释某个特定地址中的内存内容及其大小。
所以,转换(cast)其实是一种编译器指令。大部分情况下它并不改变一个指针所含的真正地址,它只影响“被指出之内存大小和其内容”的解释方式。
类型在内存中所占的空间?
类对象的空间
sizeof一个空的类型,结果是1。
理由:不包含任何信息,本来应该是0,但是声明实例时,内存占用空间,由编译器决定。VS是1。添加构造和析构函数不影响。
如果析构函数为虚函数:编译器发现类型中有虚函数,就会为该类型生成虚函数表,并为每个实例 添加指向虚函数表的指针。在32位机器上,指针占4字节,64位机器,指针占8字节。
数组的空间
注意sizeof(arr)
,当arr
为数组名称时,获取的是数组的实际大小,不是指针,虽然数组名类似于指针。
int tmpGetSize(int arr[])
{
return sizeof(arr);
}
void testArraySize()
{
int arr[6] = { 0,1,2,3,4,5 };
int size1 = sizeof(arr); //int 4 , 4*6=24
int *p = arr;
int size2 = sizeof(p); //pointer, in x64, 8
int size3 = tmpGetSize(arr);//pointer, in x64, 8
cout << "size of arr, p, and arr parameter:"
<< size1 << "," << size2 << "," << size3 << endl;
//24 , 8, 8
}
形参和实参的区别
形参是函数定义时说明,其规定了函数所接受数据的类型和数量。
实参是函数调用时的输入,实参数量与类型和形参一样,用于初始化形参。
当形参是引用类型时,对应的实参被引用传递,引用形参是实参的别名。
当形参是普通类型时,对应的实参被值传递,形参和实参相互独立。
C++中,建议使用引用类型的形参替代指针,因为使用引用,形式上更简单,无须额外声明指针变量,也避免了拷贝指针的值。如果函数无须改变引用形参的值,最好将其声明为const引用。
复制构造函数
复制构造函数必须是引用,不然由于形参的值传递会无限递归调用复制构造函数导致栈溢出。
一般采用常量引用比较好(const MyClass& b)
。
class MyClass
: public BaseClass
{
public:
MyClass() {}
MyClass(const MyClass& b) { data = b.data; }
//MyClass(MyClass b) { data = b.data; } //会报错,复制构造函数必须是引用,不然会无限递归调用导致栈溢出
private:
int data;
};
delete默认函数
在默认情况下(用户没有定义,但是也没有显示的删除),编译器会自动隐式生成一个拷贝构造函数和赋值运算符,但用户可以使用delete来指定不生成拷贝构造函数和赋值运算符,这样的对象就不能通过值传递,也不能进行赋值运算。
Person(const Person& p) = delete;
常量字符串的地址
直接将指针赋值给相同的常量字符串时,会指向相同的内存地址。
用相同的字符串给数组赋值,则因为数组各自开辟了空间,指向不同的内存地址。
void testConstArray()
{
char str1[] = "hello world";
char str2[] = "hello world";
if (str1 == str2)
cout << "str1 = str2." << endl;
else
cout << "str1 != str2." << endl; //this is right
char *p1 = "hello world";
char *p2 = "hello world";
if (p1 == p2)
cout << "p1 = p2." << endl; //this is right
else
cout << "p1 != p2." << endl;
}
函数指针
Q21剑指offer里面有一个示例,看看
变量初始化顺序
成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与定义成员变量的顺序有关。因为成员变量的初始化次序是根据变量在内存中次序有关,而内存中的排列顺序早在编译期就根据变量的定义次序决定了。这点在EffectiveC++中有详细介绍。
class TestMemInit
{
private:
int n1;
int n2;
public:
TestMemInit() : n2(0), n1(n2 + 2) {}
void printSelf()
{
cout << "n1: " << n1 << " n2:" << n2 << endl;
}
};
// 执行TestMemInit().printSelf()
//输出为: -87987678(随机数), 0
如果不使用初始化列表,则变量初始化顺序与其在构造函数中的顺序有关。
- 类中成员在定义时,是不可以初始化的
- const成员变量必须在构造函数的初始化列表中初始化
- static变量必须在类外进行初始化
多态中,成员变量的初始化顺序是:
- 基类的静态变量或全局变量
- 派生类的静态变量或全局变量
- 基类的成员变量
- 派生类的成员变量
静态变量进行初始化顺序是基类的静态变量先初始化,然后是它的派生类。直到所有的静态变量都被初始化。这里需要注意全局变量和静态变量的初始化是不分次序的。这也不难理解,其实静态变量和全局变量都被放在公共内存区。可以把静态变量理解为带有“作用域”的全局变量。在一切初始化工作结束后,main函数会被调用,如果某个类的构造函数被执行,那么首先基类的成员变量会被初始化。
const 修饰
https://blog.youkuaiyun.com/qq_41175905/article/details/81877675
修饰指针:
- 如果const在*的左侧,则const是用来修饰指针指向的变量,即指针指向的地址中存放的的值为常量;
- 如果const在*的右侧,则const是用来修饰指针本身,指针本身是常量,其指向固定。
修饰成员函数与变量:
- const 的类成员变量,只能在初始化列表中赋初值
int func() const;
常成员函数,不能修改类内的数据成员。- 类的const 对象,只能调用常成员函数??
const int func();
返回值为常量。
const int a = 2;
int b = 1;
const int* p; //指向常量的指针,指向地址内的值不可变
p = &a; //ok
// *p = 10; //error
int* const p2 = &b; //p2为常量指针,其指向的地址不可变,需要初值
// p2 = &a; //error,p2的指向不可变
*p2 = 10; //b=10,改变b的值
cout << "b:" << b << endl;
// 类
class A
{
private:
const int a; // 常对象成员,只能在初始化列表赋值
public:
// 构造函数
A() : a(0) { };
A(int x) : a(x) { }; // 初始化列表
// const可用于对重载函数的区分
int getValue(); // 普通成员函数
int getValue() const; // 常成员函数,不得修改类中的任何数据成员的值
};
void function()
{
// 对象
A b; // 普通对象,可以调用全部成员函数、更新常成员变量
const A a; // 常对象,只能调用常成员函数
const A *p = &a; // 指针变量,指向常对象
const A &q = a; // 指向常对象的引用
// 指针
char greeting[] = "Hello";
char* p1 = greeting; // 指针变量,指向字符数组变量
const char* p2 = greeting; // 指针变量,指向字符数组常量(const 后面是 char,说明指向的字符(char)不可改变)
char* const p3 = greeting; // 自身是常量的指针,指向字符数组变量(const 后面是 p3,说明 p3 指针自身不可改变)
const char* const p4 = greeting; // 自身是常量的指针,指向字符数组常量
}
// 函数
void function1(const int Var); // 传递过来的参数在函数内不可变
void function2(const char* Var); // 参数指针所指内容为常量
void function3(char* const Var); // 参数指针为常量
void function4(const int& Var); // 引用参数在函数内为常量
// 函数返回值
const int function5(); // 返回一个常数
const int* function6(); // 返回一个指向常量的指针变量,使用:const int *p = function6();
int* const function7(); // 返回一个指向变量的常指针,使用:int* const p = function7();
static
- 修饰普通变量,修改变量的存储区域和生命周期,使变量存储在静态区,在 main 函数运行前就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。
- 修饰普通函数,表明函数的作用范围,仅在定义该函数的文件内才能使用。在多人开发项目时,为了防止与他人命名空间里的函数重名,可以将函数定位为 static。
- 修饰成员变量,修饰成员变量使所有的对象只保存一个该变量,而且不需要生成对象就可以访问该成员。
- 修饰成员函数,修饰成员函数使得不需要生成对象就可以访问该函数,但是在 static 函数内不能访问非静态成员。
C 中string操作的函数
- strcpy(des, source);
- strlen()
- strcmp() ??
volatile的作用
- 防止变量被编译器优化
- 加 volatile 关键字的变量,从内存中取值,而不是寄存器。
volatile关键字是防止在共享的空间发生读取的错误。只保证其可见性,不保证原子性;使用volatile指每次从内存中读取数据,而不是从编译器优化后的缓存中读取数据,简单来讲就是防止编译器优化。
在单任务环境中,如果在两次读取变量之间不改变变量的值,编译器就会发生优化,会将RAM中的值赋值到寄存器中;由于访问寄存器的效率要高于RAM,所以在需要读取变量时,直接CPU寄存器中获取变量的值,而不是从RAM中。
在多任务环境中,虽然在两次读取变量之间不改变变量的值,在一些情况下变量的值还是会发生改变,比如在发生中断程序或者有其他的线程。这时候如果编译器优化,依旧从寄存器中获取变量的值,修改的值就得不到及时的响应(在RAM还未将新的值赋值给寄存器,就已经获取到寄存器的值)。
要想防止编译器优化,就需要在声明变量时加volatile关键字,加关键字后,就在RAM中读取变量的值,而不是直接在寄存器中取值。
pragma pack(n)
https://www.cnblogs.com/flyinggod/p/8343478.html
设定结构体、union及类成员变量以n字节的方式对齐
对齐方式:1,2,4,8,16
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GSoWdQLQ-1584275142585)(C++基础查漏补缺.assets/1220093-20180124203608740-485914586.png)]
只有long和指针在不同平台上的大小不同
#pragma pack(push) // 保存对齐状态
#pragma pack(4) // 设定为 4 字节对齐
struct test
{
char m1;
double m4;
int m3;
};
#pragma pack(pop) // 恢复对齐状态
C++ struct 和 class
struct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。
- struct: 默认继承和访问权限是 public。
- class: 默认继承和数据访问控制是 private。
union
联合(union)是一种节省空间的特殊的类,一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当某个成员被赋值后其他成员变为未定义状态。
成员变量共享一块内存。
explicit
- explicit 修饰构造函数时,可以防止隐式转换和复制初始化
- explicit 修饰转换函数时,可以防止隐式转换,但 按语境转换 除外
struct A
{
A(int) { }
operator bool() const { return true; }
};
struct B
{
explicit B(int) {}
explicit operator bool() const { return true; }
};
void doA(A a) {}
void doB(B b) {}
int main()
{
A a1(1); // OK:直接初始化
A a2 = 1; // OK:复制初始化
A a3{ 1 }; // OK:直接列表初始化
A a4 = { 1 }; // OK:复制列表初始化
A a5 = (A)1; // OK:允许 static_cast 的显式转换
doA(1); // OK:允许从 int 到 A 的隐式转换
if (a1); // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
bool a6(a1); // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
bool a7 = a1; // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
bool a8 = static_cast<bool>(a1); // OK :static_cast 进行直接初始化
B b1(1); // OK:直接初始化
B b2 = 1; // 错误:被 explicit 修饰构造函数的对象不可以复制初始化
B b3{ 1 }; // OK:直接列表初始化
B b4 = { 1 }; // 错误:被 explicit 修饰构造函数的对象不可以复制列表初始化
B b5 = (B)1; // OK:允许 static_cast 的显式转换
doB(1); // 错误:被 explicit 修饰构造函数的对象不可以从 int 到 B 的隐式转换
if (b1); // OK:被 explicit 修饰转换函数 B::operator bool() 的对象可以从 B 到 bool 的按语境转换
bool b6(b1); // OK:被 explicit 修饰转换函数 B::operator bool() 的对象可以从 B 到 bool 的按语境转换
bool b7 = b1; // 错误:被 explicit 修饰转换函数 B::operator bool() 的对象不可以隐式转换
bool b8 = static_cast<bool>(b1); // OK:static_cast 进行直接初始化
return 0;
}
friend友元函数与友元类
- 能访问私有成员
- 破坏封装性
- 友元关系不可传递,假如类B是类A的友元,类C继承于类A,那么友元类B是没办法直接访问类C的私有或保护成员。
- 友元关系的单向性,B是A的友元,但是A不是B的友元
- 友元声明的形式及数量不受限制
class A
{
public:
A(int _a):a(_a){};
friend int getA_a(A &_classA);//友元函数
friend class C; //什么友元类
private:
int a;
};
//友元函数定义
int getA_a(A &_classA)
{
return _classA.a;//通过对象名访问私有变量
}
class C
{
public:
int getA_a(A classa){
return classA.a; //返回A的私有变量
}
}
using的使用
using 指示
使得某个特定命名空间中所有名字都可见,这样我们就无需再为它们添加任何前缀限定符了,如 using namespace std
。
尽量少使用using 指示
(会污染命名空间),而使用using 声明
,如下:
using std::cin;
using std::cout;
using std::endl;
int x;
cin >> x;
cout << x << endl;
::范围解析运算符
- 全局作用域符(
::name
):用于类型名称(类、类成员、成员函数、变量等)前,表示作用域为全局命名空间 - 类作用域符(
class::name
):用于表示指定类型的作用域范围是具体某个类的 - 命名空间作用域符(
namespace::name
):用于表示指定类型的作用域范围是具体某个命名空间的
enum
限定作用域的枚举类型
enum class open_modes { input, output, append };
不限定作用域的枚举类型
enum color { red, yellow, green };
enum { floatPrec = 6, doublePrec = 10 };
decltype
decltype 关键字用于检查实体的声明类型或表达式的类型及值分类???
https://zh.cppreference.com/w/cpp/language/decltype
左值和右值
a=10;
左值是变量的地址,右值是变量存储的内容。
变量本质即存储空间的名称,编译后变为对应地址。
引用
引用本质上是一个隐式指针,为对象的一个别名,通过操作符 & 来实现。C++11又提出了左值引用与右值引用的概念,一般如没有特殊说明,提到引用都是指传统的左值引用。
https://blog.youkuaiyun.com/thisinnocence/article/details/23883483
左值引用
一个C++引用声明后必须被初始化,否则编译不过,初始化之后就相当与一个变量(地址为初始化时所引用变量的地址)。由于拥有共同的地址,而且也是同一个类型,所以对其操作就相当于对原对象的操作,用法和普通变量相同。所以,引用一般也称别名。
引用与指针最大的区别:指针是一种数据类型,而引用不是。当其用作函数传参时,传递的就是变量的左值即地址。
右值引用
C++11新特性,目的是:
- 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率;
- 能够更简洁明确地定义泛型函数;
右值引用形式:类型 && a= 被引用的对象。与左值的区别在于:右值是临时变量,如函数返回值,且不变。右值引用可以理解为右值的引用,右值初始化后临时变量消失。
#include <iostream>
using namespace std;
int glo=10;
void process(int && a){
glo+=a;
}
void process(int &a){
glo-=a;
}
int get_return(){
int b=3;
return b;
}
int main() {
int a = 10;
process(8);
cout<<glo<<endl; //18
process(a);
cout<<glo<<endl; //8
int && k = get_return();
cout<<k<<endl; //3
return 0;
}
成员初始化列表
- 高效:对于非内置类型,减少了一次调用默认构造函数的过程。
- 有些场合必须使用:
- const 常量成员,因为不能赋值,必须在初始化列表里面初始化。
- 引用类型,必须在定义时初始化。
- 没有默认构造函数的类类型,因为不需要调用默认构造函数。
##initializer_list 列表初始化
???
OOP面向对象
OOP,object-oriented programming三大特征:
- 封装,private ,protected, public
- 继承,父类 ,派生子类
- 多态
封装
把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
public
成员:可以被任意实体访问protected
成员:只允许被子类及本类的成员函数访问private
成员:只允许被本类的成员函数、友元类或友元函数访问
多态
- 多态,即多种状态(形态)。简单来说,我们可以将多态定义为消息以多种形式显示的能力。
- 多态是以封装和继承为基础的。
- C++ 多态分类及实现:
- 重载多态(Ad-hoc Polymorphism,编译期):函数重载、运算符重载
- 子类型多态(Subtype Polymorphism,运行期):虚函数
- 参数多态性(Parametric Polymorphism,编译期):类模板、函数模板
- 强制多态(Coercion Polymorphism,编译期/运行期):基本类型转换、自定义类型转换
动态多态,运行期绑定
**虚函数:**使用virtual修饰的成员函数,使其为虚函数。
- 非类成员函数不能是虚函数。
- 静态函数 static 不能是虚函数,
- 构造函数不能是虚函数(析构可以),(因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针)
- 内联函数不能是表现多态性质的虚函数
class Shape // 形状类
{
public:
virtual double calcArea()
{
...
}
virtual ~Shape();
};
class Circle : public Shape // 圆形类
{
public:
virtual double calcArea();
...
};
class Rect : public Shape // 矩形类
{
public:
virtual double calcArea();
...
};
int main()
{
Shape * shape1 = new Circle(4.0);
Shape * shape2 = new Rect(5.0, 6.0);
shape1->calcArea(); // 调用圆形类里面的方法
shape2->calcArea(); // 调用矩形类里面的方法
delete shape1;
shape1 = nullptr;
delete shape2;
shape2 = nullptr;
return 0;
}
虚析构函数
虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。
class Shape
{
public:
Shape(); // 构造函数不能是虚函数
virtual double calcArea();
virtual ~Shape(); // 虚析构函数
};
class Circle : public Shape // 圆形类
{
public:
virtual double calcArea();
...
};
int main()
{
Shape * shape1 = new Circle(4.0);
shape1->calcArea();
delete shape1; // 因为Shape有虚析构函数,所以delete释放内存时,先调用子类析构函数,再调用基类析构函数,防止内存泄漏。
shape1 = NULL;
return 0;
}
纯虚函数
纯虚函数是一种特殊的虚函数,在基类中不能对虚函数有意义的实现而将其声明为纯虚函数,实现留给基类的派生类去做。
virtual int A() = 0;
- 类里如果声明了虚函数,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖(override),这样的话,编译器就可以使用后期绑定来达到多态了。纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。
- 虚函数在子类里面可以不重写;但纯虚函数必须在子类实现才可以实例化子类。
- 虚函数的类用于 “实作继承”,继承接口的同时也继承了父类的实现。纯虚函数关注的是接口的统一性,实现由子类完成。
- 带纯虚函数的类叫抽象类,这种类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。抽象类被继承后,子类可以继续是抽象类,也可以是普通类。
- 虚基类是虚继承中的基类,具体见下文虚继承。
虚函数指针,虚函数表
- 虚函数指针:在含有虚函数的类的对象中,指向虚函数表,在运行时确定,使对象size + 指针大小(x32 4字节,x64 8字节)
- 虚函数表:在程序只读数据段,存放虚函数指针,如果派生类实现了基类的某个虚函数,在虚表中覆盖原本基类的那个虚函数指针。
- 虚函数表在对象内存的头部,顺序为: 基类的虚函数(若派生类实现了基类的虚函数,则覆盖)+自己的虚函数。
- 若派生类集成了多个基类,则有虚函数的基类在内存中靠前,没有的靠后。
虚继承
l int A() = 0;`
- 类里如果声明了虚函数,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖(override),这样的话,编译器就可以使用后期绑定来达到多态了。纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。
- 虚函数在子类里面可以不重写;但纯虚函数必须在子类实现才可以实例化子类。
- 虚函数的类用于 “实作继承”,继承接口的同时也继承了父类的实现。纯虚函数关注的是接口的统一性,实现由子类完成。
- 带纯虚函数的类叫抽象类,这种类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。抽象类被继承后,子类可以继续是抽象类,也可以是普通类。
- 虚基类是虚继承中的基类,具体见下文虚继承。
虚函数指针,虚函数表
- 虚函数指针:在含有虚函数的类的对象中,指向虚函数表,在运行时确定,使对象size + 指针大小(x32 4字节,x64 8字节)
- 虚函数表:在程序只读数据段,存放虚函数指针,如果派生类实现了基类的某个虚函数,在虚表中覆盖原本基类的那个虚函数指针。
- 虚函数表在对象内存的头部,顺序为: 基类的虚函数(若派生类实现了基类的虚函数,则覆盖)+自己的虚函数。
- 若派生类集成了多个基类,则有虚函数的基类在内存中靠前,没有的靠后。