File: README.md
- C++ 那些事
C++ 那些事
感谢各位对《C++ 那些事》的支持,现将内容也同步至B站,以视频的方式展示,可以打开网站直接阅读~欢迎大家 star、转发、PR。
在线个人博客:光城的实验室
在线学习网站:C++那些事
- 中文名:C++ 那些事
- 英文名:Stories About C Plus Plus
这是一个适合初学者从入门到进阶的仓库,解决了面试者与学习者想要深入 C++ 及如何入坑 C++ 的问题。除此之外,本仓库拓展了更加深入的源码分析,多线程并发等的知识,是一个比较全面的 C++ 学习从入门到进阶提升的仓库。
🎯 适合人群: C++ 初学者、面试准备者、想系统学习 Modern C++ 的开发者
📚 涵盖内容: C++11/14/17/20 新特性、STL 源码剖析、设计模式、并发编程
🔧 实践导向: 配套完整代码示例和实战项目
重磅项目
推出一系列重磅项目,带大家实战学习C++,配合本开源项目学习,成长飞快!
直通点:戳这里
关于作者
公众号已开放两大入口,分别是专辑与菜单,可以直接在微信公众号上阅读《C++ 那些事》内容,配上该仓库的代码,将会非常的爽,建议大家关注一波。
个人公众号:guangcity
或者扫下面的二维码,欢迎大家反馈及 C++ 交流,本人在微信创建了 C++ 那些事交流群,优质的 C++ 资源交流区,期待大家关注上面公众号,点击公众号右下角->联系我,拉你入群。
关注我
如果觉得不错,关注我吧~
| 星球 | 微信公众号 |
|---|
项目运行
方式1: vscode + bazel
方式2: docker
新推出免开发环境,docker环境,大家可以pull下面镜像:
docker pull xingfranics/cplusplusthings:latest方式3: g++
视频学习本项目
第三期:手把手共读HashTable,彻底搞懂C++ STL
第四期:手把手共读STL之enable_shared_from_this
第五期:手把手共读STL之线程,从C++11 thread到C++20 jthread
第六期:手把手共读STL之共读condition_variable、condition_variable_any
飞书知识库
基础进阶
const那些事static那些事this那些事inline那些事sizeof那些事- 函数指针那些事
- 纯虚函数和抽象类那些事
vptr_vtable那些事virtual那些事volatile那些事assert那些事- 位域那些事
extern那些事struct那些事struct与class那些事union那些事c实现c++多态那些事explicit那些事friend那些事using那些事::那些事enum那些事decltype那些事- 引用与指针那些事
- 宏那些事
实战系列
[10 日狂练](./practical_exercises/10_day_practice)
[重点实战练习](./practical_exercises/key_exercises/)
[C++2.0 新特性](./cpp2.0)
概况
C++2.0 是一个简称,意为「现代 C++」,包括 C++11/14/17/20。
[C++11 新特性](./cpp2.0/cpp11)
Spaces in Template Expressions
cppvector<list > //ok in each C++ version vector<list> // before c++ 11 error error: ‘>>’ should be ‘> >’ within a nested template argument list,c++11后可以正常通过-
cpp
for(decl:col) { statement } -
如果你自行定义了一个
ctor, 那么编译器就不会给你一个default ctor
如果强制加上=default, 就可以重新获得并使用default ctor. Alias(化名) Template (template typedef)
容器-结构与分类
- (1) 序列式容器包括:
array(C++2.0 新引入),vector,deque,list,forward_list(C++2.0 新引入) - (2) 关联式容器包括:
set/multiset,map/multimap - (3) 无序容器(C++2.0 新引入,更换原先
hash_xxx为unordered_xxx)包括:unordered_map/unordered_multimap,unordered_set/unordered_multiset
- (1) 序列式容器包括:
-
学习资料:https://www.bilibili.com/video/av51863195?from=search&seid=3610634846288253061
C++14/17/20
待更新...
设计模式
[STL 源码剖析](./src_analysis/stl)
STL 源码剖析:gcc 4.9.1
- array
- deque
- queue and stack
- list
- vector
- typename
- traits
- iterator
- 谈谈 STL 设计之 EBO 优化
- rb_tree
- set and multiset
- map and multimap
- hashtable
- myhashtable
- unordered_map
[并发编程](./concurrency)
C++ Concurrency in Action
学习资料:https://downdemo.gitbook.io/cpp-concurrency-in-action-2ed/
多线程与多进程
Threading In C++
学习自:
https://www.youtube.com/watch?v=eZ8yKZo-PGw&list=PLk6CEY9XxSIAeK-EAh3hB4fgNvYkYmghp&index=4
[C++ 惯用法](./codingStyleIdioms)
你最喜欢的 c++ 编程风格惯用法是什么?
学习课程
[极客时间《现代 C++ 实战 30 讲》](https://time.geekbang.org/channel/home)
- 堆、栈、RAII:C++ 里该如何管理资源?
- 自己动手,实现 C++ 的智能指针
- 右值和移动究竟解决了什么问题?
- 容器 1
- 容器 2
- 异常
- 字面量、静态断言和成员函数说明符
- 是不是应该返回对象?
- 编译期多态:泛型编程和模板入门
- 译期能做些什么?一个完整的计算世界
- SFINAE:不是错误的替换失败是怎么回事?
- constexpr:一个常态的世界
- 函数对象和 lambda:进入函数式编程
- 内存模型和 atomic:理解并发的复杂性
工具篇
[容器快捷输出工具](./tool/output)
对吴老师的代码进行了修改,点击此处直通代码
输入:
map mp{
{1, 1},
{2, 4},
{3, 9}};
cout << mp << endl;输出结果显示:
{ 1 => 1, 2 => 4, 3 => 9 }像 Python 一样简单输出(Jupyter Notebook)
观察编译过程变化
C++ 的 Debug 工具 dbg-macro
在 Linux 上的 Debug 工具 rr - 拥有回到过去的能力
拓展部分
一些问题
File: basic_content/vptr_vtable/README.md
深入浅出C++虚函数的vptr与vtable
关于作者:
个人公众号:
1.基础理论
为了实现虚函数,C ++使用一种称为虚拟表的特殊形式的后期绑定。该虚拟表是用于解决在动态/后期绑定方式的函数调用函数的查找表。虚拟表有时会使用其他名称,例如“vtable”,“虚函数表”,“虚方法表”或“调度表”。
虚拟表实际上非常简单,虽然用文字描述有点复杂。首先,每个使用虚函数的类(或者从使用虚函数的类派生)都有自己的虚拟表。该表只是编译器在编译时设置的静态数组。虚拟表包含可由类的对象调用的每个虚函数的一个条目。此表中的每个条目只是一个函数指针,指向该类可访问的派生函数。
其次,编译器还会添加一个隐藏指向基类的指针,我们称之为vptr。vptr在创建类实例时自动设置,以便指向该类的虚拟表。与this指针不同,this指针实际上是编译器用来解析自引用的函数参数,vptr是一个真正的指针。
因此,它使每个类对象的分配大一个指针的大小。这也意味着vptr由派生类继承,这很重要。
2.实现与内部结构
下面我们来看自动与手动操纵vptr来获取地址与调用虚函数!
开始看代码之前,为了方便大家理解,这里给出调用图:
代码全部遵循标准的注释风格,相信大家看了就会明白,不明白的话,可以留言!
/* Detailed source-code truncated for AI context efficiency. */运行结果:
基类对象直接调用
Base::fun1()
基类引用指向基类实例
Base::fun1()
基类指针指向派生类实例并调用虚函数
Derived::fun1()
基类引用指向派生类实例并调用虚函数
Derived::fun1()
=======================
vptr_addr:0x401130
func_addr:0x400ea8
Derived::fun1()
=======================
vptr_addr:0x401130
func_addr:0x400ed4
DerivedClass::fun2()我们发现C++的动态多态性是通过虚函数来实现的。简单的说,通过virtual函数,指向子类的基类指针可以调用子类的函数。例如,上述通过基类指针指向派生类实例,并调用虚函数,将上述代码简化为:
Base *pt = new Derived(); // 基类指针指向派生类实例
cout<<"基类指针指向派生类实例并调用虚函数"<<endl;
pt->fun1();其过程为:首先程序识别出fun1()是个虚函数,其次程序使用pt->vptr来获取Derived的虚拟表。第三,它查找Derived虚拟表中调用哪个版本的fun1()。这里就可以发现调用的是Derived::fun1()。因此pt->fun1()被解析为Derived::fun1()!
除此之外,上述代码大家会看到,也包含了手动获取vptr地址,并调用vtable中的函数,那么我们一起来验证一下上述的地址与真正在自动调用vtable中的虚函数,比如上述pt->fun1()的时候,是否一致!
这里采用gdb调试,在编译的时候记得加上-g。
通过gdb vptr进入gdb调试页面,然后输入b Derived::fun1对fun1打断点,然后通过输入r运行程序到断点处,此时我们需要查看调用栈中的内存地址,通过disassemable fun1可以查看当前有关fun1中的相关汇编代码,我们看到了0x0000000000400ea8,然后再对比上述的结果会发现与手动调用的fun1一致,fun2类似,以此证明代码正确!
gdb调试信息如下:
/* Detailed source-code truncated for AI context efficiency. */File: basic_content/volatile/README.md
关于作者:
个人公众号:
1.volatile
被 volatile 修饰的变量,在对其进行读写操作时,会引发一些可观测的副作用。而这些可观测的副作用,是由程序之外的因素决定的。
2.volatile应用
(1)并行设备的硬件寄存器(如状态寄存器)。
假设要对一个设备进行初始化,此设备的某一个寄存器为0xff800000。
int *output = (unsigned int *)0xff800000; //定义一个IO端口;
int init(void)
{
int i;
for(i=0;i< 10;i++)
{
*output = i;
}
}经过编译器优化后,编译器认为前面循环半天都是废话,对最后的结果毫无影响,因为最终只是将output这个指针赋值为 9,所以编译器最后给你编译编译的代码结果相当于:
int init(void)
{
*output = 9;
}如果你对此外部设备进行初始化的过程是必须是像上面代码一样顺序的对其赋值,显然优化过程并不能达到目的。反之如果你不是对此端口反复写操作,而是反复读操作,其结果是一样的,编译器在优化后,也许你的代码对此地址的读操作只做了一次。然而从代码角度看是没有任何问题的。这时候就该使用volatile通知编译器这个变量是一个不稳定的,在遇到此变量时候不要优化。
(2)一个中断服务子程序中访问到的变量;
static int i=0;
int main()
{
while(1)
{
if(i) dosomething();
}
}
/* Interrupt service routine */
void IRS()
{
i=1;
}上面示例程序的本意是产生中断时,由中断服务子程序IRS响应中断,变更程序变量i,使在main函数中调用dosomething函数,但是,由于编译器判断在main函数里面没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致dosomething永远不会被调用。如果将变量i加上volatile修饰,则编译器保证对变量i的读写操作都不会被优化,从而保证了变量i被外部程序更改后能及时在原程序中得到感知。
(3)多线程应用中被多个任务共享的变量。
当多个线程共享某一个变量时,该变量的值会被某一个线程更改,应该用 volatile 声明。作用是防止编译器优化把变量从内存装入CPU寄存器中,当一个线程更改变量后,未及时同步到其它线程中导致程序出错。volatile的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值。示例如下:
volatile bool bStop=false; //bStop 为共享全局变量
//第一个线程
void threadFunc1()
{
...
while(!bStop){...}
}
//第二个线程终止上面的线程循环
void threadFunc2()
{
...
bStop = true;
}要想通过第二个线程终止第一个线程循环,如果bStop不使用volatile定义,那么这个循环将是一个死循环,因为bStop已经读取到了寄存器中,寄存器中bStop的值永远不会变成FALSE,加上volatile,程序在执行时,每次均从内存中读出bStop的值,就不会死循环了。
是否了解volatile的应用场景是区分C/C++程序员和嵌入式开发程序员的有效办法,搞嵌入式的家伙们经常同硬件、中断、RTOS等等打交道,这些都要求用到volatile变量,不懂得volatile将会带来程序设计的灾难。
3.volatile常见问题
下面的问题可以看一下面试者是不是真正了解volatile。
(1)一个参数既可以是const还可以是volatile吗?为什么?
可以。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
(2)一个指针可以是volatile吗?为什么?
可以。尽管这并不常见。一个例子是当一个中断服务子程序修改一个指向一个buffer的指针时。
(3)下面的函数有什么错误?
int square(volatile int *ptr)
{
return *ptr * *ptr;
}这段代码有点变态,其目的是用来返回指针ptr指向值的平方,但是,由于ptr指向一个volatile型参数,编译器将产生类似下面的代码:
int square(volatile int *ptr)
{
int a,b;
a = *ptr;
b = *ptr;
return a * b;
}由于*ptr的值可能被意想不到地改变,因此a和b可能是不同的。结果,这段代码可能返回的不是你所期望的平方值!正确的代码如下:
long square(volatile int *ptr)
{
int a=*ptr;
return a * a;
}4.volatile使用
volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用 volatile 告诉编译器不应对这样的对象进行优化。
volatile 关键字声明的变量,每次访问时都必须从内存中取出值(没有被 volatile 修饰的变量,可能由于编译器的优化,从 CPU 寄存器中取值)
const 可以是 volatile (如只读的状态寄存器)
指针可以是 volatile
代码学习:
File: basic_content/virtual/README.md
virtual那些事
关于作者:
个人公众号:
1.虚函数与运行多态
对应的代码:emp.cpp
虚函数的调用取决于指向或者引用的对象的类型,而不是指针或者引用自身的类型。
2.vptr与vtable
3.虚函数中默认参数
对应的代码:default_arg.cpp
默认参数是静态绑定的,虚函数是动态绑定的。 默认参数的使用需要看指针或者引用本身的类型,而不是对象的类型。
4.可以不可以
(1) 静态函数可以声明为虚函数吗?
原因主要有两方面:
静态函数不可以声明为虚函数,同时也不能被const 和 volatile关键字修饰
static成员函数不属于任何类对象或类实例,所以即使给此函数加上virutal也是没有任何意义
虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,静态成员函数没有this指针,所以无法访问vptr。
代码学习:static_error.cpp
(2)构造函数可以为虚函数吗?
构造函数不可以声明为虚函数。同时除了inline|explicit之外,构造函数不允许使用其它任何关键字。
为什么构造函数不可以为虚函数?
尽管虚函数表vtable是在编译阶段就已经建立的,但指向虚函数表的指针vptr是在运行阶段实例化对象时才产生的。 如果类含有虚函数,编译器会在构造函数中添加代码来创建vptr。 问题来了,如果构造函数是虚的,那么它需要vptr来访问vtable,可这个时候vptr还没产生。 因此,构造函数不可以为虚函数。
我们之所以使用虚函数,是因为需要在信息不全的情况下进行多态运行。而构造函数是用来初始化实例的,实例的类型必须是明确的。 因此,构造函数没有必要被声明为虚函数。
代码学习:
(3)析构函数可以为虚函数吗?
析构函数可以声明为虚函数。如果我们需要删除一个指向派生类的基类指针时,应该把析构函数声明为虚函数。 事实上,只要一个类有可能会被其它类所继承, 就应该声明虚析构函数(哪怕该析构函数不执行任何操作)。
代码学习:
(4)虚函数可以为私有函数吗?
- 基类指针指向继承类对象,则调用继承类对象的函数;
- int main()必须声明为Base类的友元,否则编译失败。 编译器报错: ptr无法访问私有函数。 当然,把基类声明为public, 继承类为private,该问题就不存在了。
代码学习:
(5)虚函数可以被内联吗?
通常类成员函数都会被编译器考虑是否进行内联。 但通过基类指针或者引用调用的虚函数必定不能被内联。 当然,实体对象调用虚函数或者静态调用时可以被内联,虚析构函数的静态调用也一定会被内联展开。
- 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
- 内联是在编译器建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
inline virtual唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如Base::who()),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。
代码学习:
5.RTTI与dynamic_cast
RTTI(Run-Time Type Identification),通过运行时类型信息程序能够使用基类的指针或引用来检查这些指针或引用所指的对象的实际派生类型。
在面向对象程序设计中,有时我们需要在运行时查询一个对象是否能作为某种多态类型使用。与Java的instanceof,以及C#的as、is运算符类似,C++提供了dynamic_cast函数用于动态转型。相比C风格的强制类型转换和C++ reinterpret_cast,dynamic_cast提供了类型安全检查,是一种基于能力查询(Capability Query)的转换,所以在多态类型间进行转换更提倡采用dynamic_cast。
代码学习:
6.纯虚函数和抽象类
File: basic_content/using/README.md
using那些事
关于作者:
个人公众号:
基本使用
局部与全局using,具体操作与使用见下面案例:
#include
#define isNs1 1
//#define isGlobal 2
using namespace std;
void func()
{
cout<<"::func"<<endl;
}
namespace ns1 {
void func()
{
cout<<"ns1::func"<<endl;
}
}
namespace ns2 {
#ifdef isNs1
using ns1::func; /// ns1中的函数
#elif isGlobal
using ::func; /// 全局中的函数
#else
void func()
{
cout<<"other::func"<<endl;
}
#endif
}
int main()
{
/**
* 这就是为什么在c++中使用了cmath而不是math.h头文件
*/
ns2::func(); // 会根据当前环境定义宏的不同来调用不同命名空间下的func()函数
return 0;
}完整代码见:using_global.cpp
改变访问性
class Base{
public:
std::size_t size() const { return n; }
protected:
std::size_t n;
};
class Derived : private Base {
public:
using Base::size;
protected:
using Base::n;
};类Derived私有继承了Base,对于它来说成员变量n和成员函数size都是私有的,如果使用了using语句,可以改变他们的可访问性,如上述例子中,size可以按public的权限访问,n可以按protected的权限访问。
完整代码见:derived_base.cpp
函数重载
在继承过程中,派生类可以覆盖重载函数的0个或多个实例,一旦定义了一个重载版本,那么其他的重载版本都会变为不可见。
如果对于基类的重载函数,我们需要在派生类中修改一个,又要让其他的保持可见,必须要重载所有版本,这样十分的繁琐。
#include
using namespace std;
class Base{
public:
void f(){ cout<<"f()"<<endl;
}
void f(int n){
cout<<"Base::f(int)"<<endl;
}
};
class Derived : private Base {
public:
using Base::f;
void f(int n){
cout<<"Derived::f(int)"<<endl;
}
};
int main()
{
Base b;
Derived d;
d.f();
d.f(1);
return 0;
}如上代码中,在派生类中使用using声明语句指定一个名字而不指定形参列表,所以一条基类成员函数的using声明语句就可以把该函数的所有重载实例添加到派生类的作用域中。此时,派生类只需要定义其特有的函数就行了,而无需为继承而来的其他函数重新定义。
完整代码见:using_derived.cpp
取代typedef
C中常用typedef A B这样的语法,将B定义为A类型,也就是给A类型一个别名B
对应typedef A B,使用using B=A可以进行同样的操作。
typedef vector V1;
using V2 = vector;完整代码见:using_typedef.cpp
File: basic_content/union/README.md
UNION那些事
关于作者:
个人公众号:
联合(union)是一种节省空间的特殊的类,一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当某个成员被赋值后其他成员变为未定义状态。联合有如下特点:
- 默认访问控制符为 public
- 可以含有构造函数、析构函数
- 不能含有引用类型的成员
- 不能继承自其他类,不能作为基类
- 不能含有虚函数
- 匿名 union 在定义所在作用域可直接访问 union 成员
- 匿名 union 不能包含 protected 成员或 private 成员
- 全局匿名联合必须是静态(static)的
File: basic_content/this/README.md
this指针那些事
关于作者
微信公众号:
1.this指针
相信在坐的很多人,都在学Python,对于Python来说有self,类比到C++中就是this指针,那么下面一起来深入分析this指针在类中的使用!
首先来谈谈this指针的用处:
(1)一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。
(2)this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针,编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行。
其次,this指针的使用:
(1)在类的非静态成员函数中返回类对象本身的时候,直接使用 return *this。
(2)当参数与成员变量名相同时,如this->n = n (不能写成n = n)。
另外,在网上大家会看到this会被编译器解析成A *const ,A const * ,究竟是哪一个呢?下面通过断点调试分析:
现有如下例子:
#include
#include<cstring>
using namespace std;
class Person{
public:
typedef enum {
BOY = 0,
GIRL
}SexType;
Person(char *n, int a,SexType s){
name=new char[strlen(n)+1];
strcpy(name,n);
age=a;
sex=s;
}
int get_age() const{
return this->age;
}
Person& add_age(int a){
age+=a;
return *this;
}
~Person(){
delete [] name;
}
private:
char * name;
int age;
SexType sex;
};
int main(){
Person p("zhangsan",20,Person::BOY);
cout<<p.get_age()<<endl;
cout<<p.add_age(10).get_age()<<endl;
return 0;
}对于这个简单的程序,相信大家没得问题吧,就是定义了一个类,然后初始化构造函数,并获取这个人的年龄,设置后,再获取!
为了验证this指针是哪一个,现在在add_age处添加断点,运行后如下:
会发现编译器自动为我们加上A* const,而不是A const *this!
紧接着,上述还有个常函数,那么我们在对get_age添加断点,如下:
会发现编译器把上述的this,变为const A* const,这个大家也能想到,因为这个函数是const函数,那么针对const函数,它只能访问const变量与const函数,不能修改其他变量的值,所以需要一个this指向不能修改的变量,那就是const A*,又由于本身this是const指针,所以就为const A* const!
总结:this在成员函数的开始执行前构造,在成员的执行结束后清除。上述的get_age函数会被解析成get_age(const A * const this),add_age函数会被解析成add_age(A* const this,int a)。在C++中类和结构是只有一个区别的:类的成员默认是private,而结构是public。this是类的指针,如果换成结构,那this就是结构的指针了。
File: basic_content/struct_class/README.md
struct与class区别
关于作者:
个人公众号:
关于C与C++中struct内容:见struct那些事
总的来说,struct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。
区别:
最本质的一个区别就是默认的访问控制
默认的继承访问权限。struct 是 public 的,class 是 private 的。
struct 作为数据结构的实现体,它默认的数据访问控制是 public 的,而 class 作为对象的实现体,它默认的成员变量访问控制是 private 的。
File: basic_content/struct/README.md
一文搞懂C和C++中struct
关于作者:
个人公众号:
1.C中struct
- 在C中struct只单纯的用作数据的复合类型,也就是说,在结构体声明中只能将数据成员放在里面,而不能将函数放在里面。
- 在C结构体声明中不能使用C++访问修饰符,如:public、protected、private 而在C++中可以使用。
- 在C中定义结构体变量,如果使用了下面定义必须加struct。
- C的结构体不能继承(没有这一概念)。
- 若结构体的名字与函数名相同,可以正常运行且正常的调用!例如:可以定义与 struct Base 不冲突的 void Base() {}。
完整案例:
#include<stdio.h>
struct Base { // public
int v1;
// public: //error
int v2;
//private:
int v3;
//void print(){ // c中不能在结构体中嵌入函数
// printf("%s\n","hello world");
//}; //error!
};
void Base(){
printf("%s\n","I am Base func");
}
//struct Base base1; //ok
//Base base2; //error
int main() {
struct Base base;
base.v1=1;
//base.print();
printf("%d\n",base.v1);
Base();
return 0;
}最后输出:
1
I am Base func完整代码见:struct_func.c
2.C++中struct
与C对比如下:
- C++结构体中不仅可以定义数据,还可以定义函数。
- C++结构体中可以使用访问修饰符,如:public、protected、private 。
- C++结构体使用可以直接使用不带struct。
- C++继承
- 若结构体的名字与函数名相同,可以正常运行且正常的调用!但是定义结构体变量时候只能用带struct的!
例如:
情形1:不适用typedef定义结构体别名
未添加同名函数前:
struct Student {
};
//Student(){}
struct Student s; //ok
Student s; //ok添加同名函数后:
struct Student {
};
Student(){}
struct Student s; //ok
Student s; //error情形二:使用typedef定义结构体别名
typedef struct Base1 {
int v1;
int v3;
public: //显示声明public
int v2;
void print(){
printf("%s\n","hello world");
};
}B;
//void B() {} //error! 符号 "B" 已经被定义为一个 "struct Base1" 的别名前三种案例
#include
#include<stdio.h>
struct Base {
int v1;
// private: //error!
int v3;
public: //显示声明public
int v2;
void print(){
printf("%s\n","hello world");
};
};
int main() {
struct Base base1; //ok
Base base2; //ok
Base base;
base.v1=1;
base.v3=2;
base.print();
printf("%d\n",base.v1);
printf("%d\n",base.v3);
return 0;
}完整代码见:struct_func.cpp
继承案例
#include
#include<stdio.h>
struct Base {
int v1;
// private: //error!
int v3;
public: //显示声明public
int v2;
virtual void print(){
printf("%s\n","Base");
};
};
struct Derived:Base {
public:
int v2;
void print(){
printf("%s\n","Derived");
};
};
int main() {
Base *b=new Derived();
b->print();
return 0;
}完整代码见:ext_struct_func.cpp
同名函数
#include
#include<stdio.h>
struct Base {
int v1;
// private: //error!
int v3;
public: //显示声明public
int v2;
void print(){
printf("%s\n","hello world");
};
};
typedef struct Base1 {
int v1;
// private: //error!
int v3;
public: //显示声明public
int v2;
void print(){
printf("%s\n","hello world");
};
}B;
void Base(){
printf("%s\n","I am Base func");
}
//void B() {} //error! 符号 "B" 已经被定义为一个 "struct Base1" 的别名
int main() {
struct Base base; //ok
//Base base1; // error!
base.v1=1;
base.v3=2;
base.print();
printf("%d\n",base.v1);
printf("%d\n",base.v3);
Base();
return 0;
}完整代码见:struct_func_func.cpp
3.总结
C和C++中的Struct区别
| C | C++ |
|---|---|
| 不能将函数放在结构体声明 | 能将函数放在结构体声明 |
| 在C结构体声明中不能使用C++访问修饰符。 | public、protected、private 在C++中可以使用。 |
| 在C中定义结构体变量,如果使用了下面定义必须加struct。 | 可以不加struct |
| 结构体不能继承(没有这一概念)。 | 可以继承 |
| 若结构体的名字与函数名相同,可以正常运行且正常的调用! | 若结构体的名字与函数名相同,使用结构体,只能使用带struct定义! |
File: basic_content/static/README.md
static那些事
关于作者
微信公众号:
当与不同类型一起使用时,Static关键字具有不同的含义。我们可以使用static关键字:
静态变量: 函数中的变量,类中的变量
静态类的成员: 类对象和类中的函数
现在让我们详细看一下静态的这些用法:
静态变量
- 函数中的静态变量
当变量声明为static时,空间将在程序的生命周期内分配。即使多次调用该函数,静态变量的空间也只分配一次,前一次调用中的变量值通过下一次函数调用传递。这对于在C / C ++或需要存储先前函数状态的任何其他应用程序非常有用。
#include
#include <string>
using namespace std;
void demo()
{
// static variable
static int count = 0;
cout << count << " ";
// value is updated and
// will be carried to next
// function calls
count++;
}
int main()
{
for (int i=0; i<5; i++)
demo();
return 0;
}输出:
0 1 2 3 4您可以在上面的程序中看到变量count被声明为static。因此,它的值通过函数调用来传递。每次调用函数时,都不会对变量计数进行初始化。
- 类中的静态变量
由于声明为static的变量只被初始化一次,因为它们在单独的静态存储中分配了空间,因此类中的静态变量由对象共享。对于不同的对象,不能有相同静态变量的多个副本。也是因为这个原因,静态变量不能使用构造函数初始化。
#include
using namespace std;
class Apple
{
public:
static int i;
Apple()
{
// Do nothing
};
};
int main()
{
Apple obj1;
Apple obj2;
obj1.i =2;
obj2.i = 3;
// prints value of i
cout << obj1.i<<" "<<obj2.i;
}您可以在上面的程序中看到我们已经尝试为多个对象创建静态变量i的多个副本。但这并没有发生。因此,类中的静态变量应由用户使用类外的类名和范围解析运算符显式初始化,如下所示:
#include
using namespace std;
class Apple
{
public:
static int i;
Apple()
{
// Do nothing
};
};
int Apple::i = 1;
int main()
{
Apple obj;
// prints value of i
cout << obj.i;
}输出:
1静态成员
- 类对象为静态
就像变量一样,对象也在声明为static时具有范围,直到程序的生命周期。
考虑以下程序,其中对象是非静态的。
#include
using namespace std;
class Apple
{
int i;
public:
Apple()
{
i = 0;
cout << "Inside Constructor\n";
}
~Apple()
{
cout << "Inside Destructor\n";
}
};
int main()
{
int x = 0;
if (x==0)
{
Apple obj;
}
cout << "End of main\n";
}输出:
Inside Constructor
Inside Destructor
End of main在上面的程序中,对象在if块内声明为非静态。因此,变量的范围仅在if块内。因此,当创建对象时,将调用构造函数,并且在if块的控制权越过析构函数的同时调用,因为对象的范围仅在声明它的if块内。
如果我们将对象声明为静态,现在让我们看看输出的变化。
#include
using namespace std;
class Apple
{
int i;
public:
Apple()
{
i = 0;
cout << "Inside Constructor\n";
}
~Apple()
{
cout << "Inside Destructor\n";
}
};
int main()
{
int x = 0;
if (x==0)
{
static Apple obj;
}
cout << "End of main\n";
}输出:
Inside Constructor
End of main
Inside Destructor您可以清楚地看到输出的变化。现在,在main结束后调用析构函数。这是因为静态对象的范围是贯穿程序的生命周期。
- 类中的静态函数
就像类中的静态数据成员或静态变量一样,静态成员函数也不依赖于类的对象。我们被允许使用对象和'.'来调用静态成员函数。但建议使用类名和范围解析运算符调用静态成员。
允许静态成员函数仅访问静态数据成员或其他静态成员函数,它们无法访问类的非静态数据成员或成员函数。
#include
using namespace std;
class Apple
{
public:
// static member function
static void printMsg()
{
cout<<"Welcome to Apple!";
}
};
// main function
int main()
{
// invoking a static member function
Apple::printMsg();
}输出:
Welcome to Apple!限定访问范围
static还有限定访问范围的作用(类似于匿名名字空间)。来自issue:https://github.com/Light-City/CPlusPlusThings/issues/142
// source1.cpp
extern void sayHello();
const char* msg = "Hello World!\n";
int main()
{
sayHello();
return 0;
}
// source2.cpp
#include <cstdio>
extern char* msg;
void sayHello()
{
printf("%s", msg);
}g++对于上面两个代码文件是可以正常编译并且打印Hello World!,但如果给source1.cpp中的msg加上static,则会导致undefined reference to 'msg'的编译错误:
// source1.cpp
extern void sayHello();
static const char* msg = "Hello World!\n";
int main()
{
sayHello();
return 0;
}