C/C++知识点总结(四)
用C++实现一个无法继承的对象
首先想到的是在C++中,在子类构造函数中会隐式调用父类的构造函数。同样地,在子类析构函数中也会隐式调用父类的析构函数。若要使一个类无法继承自身,则需将它的构造函数和析构函数声明为私有类型 。当一个类试图继承自身时,则必然会因尝试调用这些虚函数而导致编译错误。
由于该类的构造与析构函数均为私有函数, 因此无法直接访问其对象实例. 那么, 如何获取该对象的具体实例呢? 例如, 在代码中可以通过声明静态成员变量并使用关键字new来动态创建相应的对象实例. 按照这一方法, 可以写出如下的代码:
class FinalClass1
{
public :
static FinalClass1* GetInstance()
{
return new FinalClass1;
}
static void DeleteInstance( FinalClass1* pInstance)
{
delete pInstance;
pInstance = 0;
}
private :
FinalClass1() {}
~FinalClass1() {}
};
该类无法继承,在感觉它与常规的类存在差异的同时也存在一些不便之处。例如,在这种情况下仅能获取堆内的实例而无法从栈中获取实例。是否可以设计一种类似常规类但仅缺少继承功能的新类型?
#include<iostream>
using namespace std;
template <typename T>
class Base
{
friend T;
private:
Base() {}
~Base() {}
};
class Finalclass : virtual public Base<Finalclass>
{
public:
Finalclass() {}
~Finalclass() {}
};
class TestClass : public Finalclass
{
//TestClass(){}继承时报错,无法通过编译
};
int main()
{
Finalclass* p = new Finalclass; // 堆上对象
Finalclass fs; // 栈上对象
// TestClass tc; // 基类构造函数私有,不可以被继承。因此不可以创建栈上对象。
system("pause");
return 0;
}
Final class inherits from Base class, where Base is a virtual base class. Due to the friendship principle, it can access private constructors and destructors of its base class. The compilation process is correct. Furthermore, it is possible to create objects in memory (heap) and establish corresponding objects on the method call stack.
为什么必须是虚继承(virtual)?
一般情况下,默认情况下每个类型只会对其直接基底进行一次对象构造
为什么Finalclass类不能被继承?
由于Finalclass与Base互为友元关系,在理论上即使不考虑实现细节的情况下也能实现正常的对象创建。然而由于Finalclass采用了虚继承机制,在实际应用中如果试图通过测试用例类(TestClass)来继承并使用Base类的一些功能时就会遇到困难。需要注意的是,在C++语言中这种友元关系无法被继承(PS:这是一个重要的注意事项)。因此,在测试时需要确保TestClass继承了Base,并且其构造函数会调用Base的构造函数。同时由于Base类的构造函数是私有的性质导致直接访问可能会引发权限问题,并最终导致编译器在构建过程中出现错误。为了避免这种情况的发生必须采取适当的保护措施以确保这种特殊的访问控制机制能够得到正确应用
二,多重类构造和析构的顺序
http://gaocegege.com/Blog/cpp/cppclass
当类进行构造时,在编译阶段会按照以下步骤依次调用各个相关部分:首先调用基类( virtual inheritance)的构造函数;接着按从左到右的顺序调用非基类( ordinary inheritance)各父类构造函数;随后依次进行数据成员初始化(与参数列表无关);最后调用自身的构造函数。而对于析构过程则与此相反,遵循镜像关系。
三,类的sizeof大小
<>
必须明确的一个概念是:通常所说的类仅是类型的一种标识,并不具备大小属性。然而,在本语境中我们讨论的是关于类对象占用空间的情况。因此当我们使用sizeof运算符作用于一个类型名时,则会得到与该类型相关联的空间占用情况。
尽管是一个空类,在内存中为每个对象分配独立的一块存储空间也是一个必要的操作其实在内存中为每个对象分配独立且唯一的内存地址是实现多线程安全的重要基础同样地,在编译器层面对于类型系统而言如果一个类型没有任何成员或属性那么它就是一个所谓的'虚无缥缈的对象'因此在C++语言规范中为了防止不同线程之间出现数据竞争导致的应用崩溃编译器会对这些虚无缥缈的对象进行特殊处理具体来说会向其隐式地附加至少一个字节以确保这些对象具有独立性和唯一性这样一来,在编译器层面会自动给这些虚无缥缈的对象分配独立的空间以避免冲突最后我们可以通过 sizeof 运算符来验证这一特性从而确定这种类型的大小确实是1个字节
- 类的对象大小等于其非静态成员数据类型所占用的空间之总和因此无需考虑静态成员的数据占用
静态数据成员之所以不计算在对象尺寸中是因为它们属于所有实例共有的资源即每个实例都能访问同一个存储空间区域而并非特定于任何一个实例的数据 - 普通 member function 并不受 sizeof 运算影响
- 虚 function 必须存在于 virtual function table 中以供继承方程组使用 因此每个 virtual function 占用4个字节的空间用于存储相应的表信息
- 整个 class 对象同样遵循类似的内存对齐策略进行规划 这种策略有助于提高程序运行效率并减少潜在的数据干扰风险
四,字节对齐问题
<>
在没有#pragma pack宏的情况下
- 数据成员对齐规则:在结构体中各字段首地址必须满足各自字段长度为整数倍的要求。
- 结构体的整体尺寸即为 sizeof 的结果,并且必须满足所有字段长度的最大值为其整数倍;如果计算结果小于该最大值,则需补足。
typedef struct bb
{
int id; //[0]....[3]
double weight; //[8].....[15] 原则1
float height; //[16]..[19],总长要为8的整数倍,补齐[20]...[23] 原则2
}BB;
typedef struct aa
{
char name[2]; //[0],[1]
int id; //[4]...[7] 原则1
double score; //[8]....[15] 原则1
short grade; //[16],[17]
BB b; //[24]......[47] 原则1
}AA;
int main()
{
AA a;
cout<<sizeof(a)<<" "<<sizeof(BB)<<endl;
return 0;
}
结果是:
48 24
在代码前加一句#pragma pack(1),你会很高兴的发现,上面的代码输出为
32 16
bb是4+8+4=16,aa是2+4+8+2+16=32;
注:改写时保留了数学公式...和英文内容不变,并通过调整语序和词汇使表述更加丰富自然
五,C++ 四种强制转换类型函数
<>
https://www.cnblogs.com/Allen-rg/p/6999360.html
<>
- 丢弃常数属性时使用\texttt{const cast}。
- 初等类型转换通常采用\texttt{static cast}。
- 多态类之间的类型转换应采用\texttt{dynamic cast}。
- 根据不同指针的类型进行重新解释时使用\texttt{reinterpret cast}。
1.const_cast(编译器在编译期处理)
- 常量指数表被转译为非定性的指数表的同时依然指向原始的对象;
- 常量子词性转移器也被转译为非定性的量子词性转移器的同时依然指向原始的对象;
- 该方法通常用于对
$index$类型的变量进行初始化。
index必须是可初始化变量或者具有初始值属性的对象才能使用此方法实现初始化功能。
#include<iostream>
int main() {
// 原始数组
int ary[4] = { 1,2,3,4 };
// 打印数据
for (int i = 0; i < 4; i++)
std::cout << ary[i] << "\t";
std::cout << std::endl;
// 常量化数组指针
const int*c_ptr = ary;
//c_ptr[1] = 233; //error
// 通过const_cast<Ty> 去常量
int *ptr = const_cast<int*>(c_ptr);
// 修改数据
for (int i = 0; i < 4; i++)
ptr[i] += 1; //pass
// 打印修改后的数据
for (int i = 0; i < 4; i++)
std::cout << ary[i] << "\t";
std::cout << std::endl;
return 0;
}
/* out print
1 2 3 4
2 3 4 5
*/
2.static_cast(编译器在编译期处理)
static_cast 的功能类似于C语言风格强制转换的效果。因为没有运行时类型检查以确保这种转化的安全性,
此类强制转化与C语言风格的强制转化都存在安全隐患。
在基类(父类)与派生类(子类)之间指针或引用的转型中,
上行转型是安全的;
下行转型因缺乏动态类型检查而不可靠。
对于基本数据类型的转化,
如将int转化为char,
或者将int转化为enum,
这种转化的安全性需由开发者自行管理。
在c++ primer 中所述:
在C++中所有隐式的转态都是通过 static_cast 来实现的。
/* 常规的使用方法 */
float f_pi=3.141592f
int i_pi=static_cast<int>(f_pi); /// i_pi 的值为 3
/* class 的上下行转换 */
class Base{
// something
};
class Sub:public Base{
// something
}
// 上行 Sub -> Base
//编译通过,安全
Sub sub;
Base *base_ptr = static_cast<Base*>(&sub);
18. // 下行 Base -> Sub
//编译通过,不安全
Base base;
Sub *sub_ptr = static_cast<Sub*>(&base);
3.dynamic_cast(在运行期,会检查这个转换是否可能)
在C++编程中,_dynamic_cast_操作强制执行类型转换,应该被视为这四种操作中最为特别的一种,由于它涉及到了面向对象编程中的多态性以及程序运行时的具体状态,同时也与编译器的某些属性设置密切相关 。
(1)其余三种均在编译阶段完成;dynamic_cast是在运行时处理的方式;在运行时期需执行类型验证。
(2)不能用于内置的基本数据类型的强制转换。
当dynamic_cast转换在操作上是有效的时,它将返回指向该类的指针或引用;如果转换是无效的,则会返回NULL.
(4)使用dynamic_cast进行转换的,基类中一定要有虚函数,否则编译不通过。
#include<iostream>
using namespace std;
class Base{
public:
Base() {}
~Base() {}
void print() {
std::cout << "I'm Base" << endl;
}
virtual void i_am_virtual_foo() {}
};
class Sub: public Base{
public:
Sub() {}
~Sub() {}
void print() {
std::cout << "I'm Sub" << endl;
}
virtual void i_am_virtual_foo() {}
};
int main() {
cout << "Sub->Base" << endl;
Sub * sub = new Sub();
sub->print();
Base* sub2base = dynamic_cast<Base*>(sub);
if (sub2base != nullptr) {
sub2base->print();
}
cout << "<sub->base> sub2base val is: " << sub2base << endl;
cout << endl << "Base->Sub" << endl;
Base *base = new Base();
base->print();
Sub *base2sub = dynamic_cast<Sub*>(base);
if (base2sub != nullptr) {
base2sub->print();
}
cout <<"<base->sub> base2sub val is: "<< base2sub << endl;
delete sub;
delete base;
return 0;
}
/* vs2017 输出为下
Sub->Base
I'm Sub
I'm Base
<sub->base> sub2base val is: 00B9E080 // 注:这个地址是系统分配的,每次不一定一样
Base->Sub
I'm Base
<base->sub> base2sub val is: 00000000 // VS2017的C++编译器,对此类错误的转换赋值为nullptr
*/
通过查看代码及其运行结果可以看出:当使用动态_cast将子类指针转换为基类指针时,并未报错且运行正常;而当尝试将基类指针转换为子类指针时,dynamic_cast并未报错但返回的base2sub值为nullptr,这表明dynamic_cast在程序运行期间执行了类型转换所需的"运行期类型信息"(Runtime type information,RTTI)检查.这种检查机制源于C++面向对象编程中虚函数的作用.具体而言,当某个类继承自另一个类并且定义了一个与父类相同名称及参数签名的成员函数重写父类的方法时,编译器会生成一个方法表(virtual method table)以记录这些函数的具体地址.因此,当我们将父类的指针或引用指向子对象时,dynamic_cast能够沿着方法表找到相应的子对象方法而非父对象方法从而实现多态性功能.在本份代码中,BASE和SUB两类都声明并实现了名为'i_am_virtual_foo'的一个虚拟函数.这意味着在使用dynamic_cast进行类型转换的过程中,BASE和SUB都会进行相应的RTTI检查这一特性也是我们所观察到的现象的原因所在
4.reinterpret_cast(编译器在编译期处理)
reinterpret_cast是强制类型转换符用来处理无关类型转换的。
通过调整指针或引用的数据类型来实现相关操作;将目标数据类型的地址赋值给当前变量;通过变量赋值的方式实现数据类型的转变。
该类型的变量只能是以下几种形式之一:包括指向器对象、引用对象、算术类型对象、函数返回值以及成员指向器对象等特定数据形式中的某一种类别。这种类型的变量不仅可以将另一个指向器转化为对应的整数值(即通过先获取指向器所指向的对象的索引值这一中间结果,并随后构造一个新的指向器变量),还可以将指定的一个整数值转化为相应的指向器对象(即先获取原始指向器对象所对应的索引值,并以此为基础生成新的指向器变量),从而最终能够完整地保留原始的初始指向信息。
#include<iostream>
#include<cstdint>
using namespace std;
int main() {
int *ptr = new int(233);
uint32_t ptr_addr = reinterpret_cast<uint32_t>(ptr);
cout << "ptr 的地址: " << hex << ptr << endl
<< "ptr_addr 的值(hex): " << hex << ptr_addr << endl;
delete ptr;
return 0;
}
/*
ptr 的地址: 0061E6D8
ptr_addr 的值(hex): 0061e6d8
*/
六,指针和引用的区别(一般都会问到)
1. 相同点:
- 在内存地址系统中涉及的概念;
- 指针指向特定的一块内存区域;
- 其存储的内容即为该内存块对应的地址值;
- 引用则可被视为对另一块内存区域的不同名称或标识符。
2. 区别:
- 虽然指针被视为一个实体数据类型,在C语言中它代表一块内存空间,并且可以被赋值和操作;而引用则是用于标识特定变量或对象的名字。
- 在C++中,默认情况下引用不需要解引用操作。
- 与之相反的是,在C++中一旦定义好后就不能再更改其值。
- 同样地,在C++编程中这使得程序运行时不会出现空引用的情况。
- “sizeof 引用”返回的是目标对象占用的空间大小。
- 它们对内存地址的操作方式也存在差异。
- 从内存管理的角度来看,在C++中为指针变量分配内存区域是必须的操作。
七.C++中Overload、Overwrite及Override的区别
1.Overload(重载):
在C++编程中可以实现多个语义与功能相近的函数通过同名来实现这一机制称为函数重载这些函数虽然名称完全一致但其参数和返回值虽有差异涵盖类型和顺序的变化这一特性使得它们能够在不同的场景下执行特定的操作
2.Override(覆盖):
涉及派生类与基类之间的特定接口实现关系。其主要特征包括:
(1)功能分布层次分明(分别位于派生级和基级);
(2)名称一致;
(3)参数配置一致;
(4)其中,基级的函数必须声明为virtual关键字。
3.Overwrite(重写):
该段代码展示了继承机制中派生类如何覆盖基类的方法。规则如下:
(1)当两个方法名称一致但参数类型不同时,在编译时无论是否声明为虚拟方法都会使基类的方法被覆盖以供编译器调用,默认情况下不会引发错误提示信息。
(2)若这两个方法不仅名称相同且参数完全一致,则只有当基方法声明了虚拟关键字时才会被覆盖;否则即使声明为虚拟也不会影响结果。
#include <stdio.h>
#include <iostream>
using namespace std;
class Parent
{
public:
void F()
{
printf("Parent.F()/n");
}
virtual void G()
{
printf("Parent.G()/n");
}
int Add(int x, int y)
{
return x + y;
}
//重载(overload)Add函数
float Add(float x, float y)
{
return x + y;
}
};
class ChildOne:Parent
{
//重写(overwrite)父类函数
void F()
{
printf("ChildOne.F()/n");
}
//覆写(override)父类虚函数,主要实现多态
void G()
{
printf("ChildOne.G()/n");
}
};
int main()
{
ChildOne childOne;// = new ChildOne();
Parent* p = (Parent*)&childOne;
//调用Parent.F()
p->F();
//实现多态
p->G();
Parent* p2 = new Parent();
//重载(overload)
printf("%d/n",p2->Add(1, 2));
printf("%f/n",p2->Add(3.4f, 4.5f));
delete p2;
system("PAUSE");
return 0;
}
八,写string类的构造,析构,拷贝函数和赋值函数
class String
{
public:
String(const char *str=NULL); //构造函数
String(const String &other); //拷贝构造函数
~String(void); //析构函数
String& operator=(const String &other); //赋值函数
ShowString();
private:
char *m_data; //指针
};
String::~String()
{
delete [] m_data; //析构函数,释放地址空间
}
String::String(const char *str)
{
if (str==NULL)//当初始化串不存在的时候,为m_data申请一个空间存放'\0';
{
m_data=new char[1];
*m_data='\0';
}
else//当初始化串存在的时候,为m_data申请同样大小的空间存放该串;
{
int length=strlen(str);
m_data=new char[length+1];
strcpy(m_data,str);
}
}
String::String(const String &other)//拷贝构造函数,功能与构造函数类似。
{
int length=strlen(other.m_data);
m_data=new [length+1];
strcpy(m_data,other.m_data);
}
String& String::operator =(const String &other)
{
if (this==&other)//当地址相同时,直接返回;
return *this;
if(m_data != NULL)
delete [] m_data;//当地址不相同时,删除原来申请的空间,重新开始构造;
int length=strlen(other.m_data);
m_data=new [length+1];
strcpy(m_data,other.m_data);
return *this;
}
String::ShowString()//由于m_data是私有成员,对象只能通过public成员函数来访问;
{
cout<<this->m_data<<endl;
}
main()
{
String AD;
char * p="ABCDE";
String B(p);
AD.ShowString();
AD=B;
AD.ShowString();
}
九,C++拷贝和赋值的区别
https://www.cnblogs.com/wangguchangqing/p/6141743.html
所使用的构造函数是拷贝构造函数还是赋值运算符,则取决于是否存在新的对象实例被创建出来。如果有新对象生成,则采用了拷贝构造函数;否则,则进行了赋值操作以引用已存在的对象实例。
调用拷贝构造函数主要有以下场景:
- 被指定为函数参数,并通过值传递的方式接受输入。
- 被指定为函数返回值,并通过值的形式从函数中输出。
- 将一个对象用作另一个对象的初始化源。
#include <iostream>
using namespace std;
class Person
{
public:
Person(){}
Person(const Person& p)
{
cout << "Copy Constructor" << endl;
}
Person& operator=(const Person& p)
{
cout << "Assign" << endl;
return *this;
}
private:
int age;
string name;
};
void f(Person p)
{
return;
}
Person f1()
{
Person p;
return p;
}
int main()
{
Person p;
Person p1 = p; // 1
Person p2;
p2 = p; // 2
f(p2); // 3
p2 = f1(); // 4
Person p3 = f1(); // 5
return 0;
}
分析如下:
虽然调用了赋值运算符"="来复制变量的值。
随后声明了一个新变量p2。
将变量p的当前值复制给新声明的对象p2。
通过传递值的方式向函数f传递参数。
函数f1返回了一个Person类型的临时对象作为其参数。
深拷贝、浅拷贝
了解复制构造函数时,必须先掌握深复制与浅复 制的概念。在编程语言中,默认生成的复 制构 造 函数 和赋 值运 算符,只 能实 现 值 的简单 复制 。举个例子来说,在如上所示 的 Person 类中,该类仅包含两个字段类型:整 数型 与字符串型。在这 进行 值 复 复 制 后生 成的对象 与 源 对象之间没有任何关联,因此 源 对象的操作不会影响到被复 制 出的对象。另一方面,在如果一个 Person 对象拥有指向整 数类型的成员变量时,这时 在 执行 浅 复 复 制 时仍然仅 进 行 值 复制 ,则 生成的新 Person 对象中的整 数指针 将指向 与 源 对象相同的内存位置。任何对这个指针字段进行修改的行为都会导致源 对象与新副本之间的数据变化 。因此,这种情况就是浅复 制 或者 浅 拷 贝的情景 ,而 另一种情况则是深-复 制 或者深 - 拷 贝的情景 。
主要针对类中涉及的指针及动态内存分配的空间设计
- 具有指针类型的成员以及包含动态内存分配的行为都应编写自定义的拷贝构造函数
- 编写自定义拷贝构造函数时,请务必同时实现赋值运算符操作符
对于拷贝构造函数的实现要确保以下几点:
- 对所有属于值类型的数据成员进行赋值操作
- 在对象复制过程中涉及的指针和动态内存区域必须提前释放并重新获取
- 为了确保子类对象正确继承父对象的行为特征,在子对象构造过程中必须先调用相应的构造函数
十,大端小端
- Little-Endian其特点是将低字节存储于内存的低地址端,并将高字节数组存储于高地址端。
- Big-Endian其特点是将高字节存储于内存的低地址端,并将低 字节数组存储于高地址 端。
判断大小端
BOOL IsBigEndian()
{
int a = 0x1234;
char b = *(char *)&a; //通过将int强制类型转换成char单字节,通过判断起始存储位置。即等于 取b等于a的低地址部分
if( b == 0x12)
{
return TRUE;
}
return FALSE;
}
联盟体union的存储安排是所有成员均从低地址开始存储,并且该结构能够有效地决定了CPU对内存采用Little-endian还是Big-endian模式进行读写操作。
BOOL IsBigEndian()
{
union NUM
{
int a;
char b;
}num;
num.a = 0x1234;
if( num.b == 0x12 )
{
return TRUE;
}
return FALSE;
}
一般操作系统都是小端,而通讯协议是大端的。
十一,守护进程
<>
在后台运行时会避免通过终端界面与用户进行交互;该程序将在被调用的那一刻开始执行,并一直到整个系统的关闭时刻才会退出。
为确保与之前环境的隔离性, 守护进程应采取措施与其运行前的环境隔离开来. 这些具体环境包括未被关闭的文件描述符、控制终端、会话与进程组、工作目录以及文件创建掩模等. 这些环境中许多项目通常是守护进程中由其父进程中所继承的状态.
创建一个简单的守护进程:
- step1:生成子进程,并使父进程退出:(模拟场景表明父进程中已完成操作并可安全退出终端)
- step2:通过调用系统函数setid()来创建新会话:实现进程组及会话期的转换
- step3:切换当前目录至根目录,并指出通过fork创建的子进程中继承了父进程中所设定的当前工作目录
- step4:执行重新设置文件权限掩码指令:umask(0)
- step5:终止所有打开的文件描述符
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#define MAXFILE 65535
int main()
{
pid_t pc;
int i, fd, len;
char *buf="this is a Dameon\n";
len = strlen(buf);
pc = fork(); //第一步
if(pc<0){
printf("error fork\n");
exit(1);
}
else if(pc>0)
exit(0);
setsid(); //第二步
chdir("/"); //第三步
umask(0); //第四步
for(i=0;i<MAXFILE;i++) //第五步
close(i);
if((fd=open("/tmp/dameon.log",O_CREAT|O_WRONLY|O_APPEND,0600))<0)
{
perror("open");
exit(1);
}
while(1)
{
write(fd,buf,len+1);
sleep(10);
}
}
