cppNotes
初始化列表
在C++中,初始化列表(initializer list)是用于初始化类成员变量的一种方式。以下是必须在初始化列表中初始化的情况:
常量成员变量(const):由于常量成员变量在创建对象时必须进行初始化,因此它们必须在初始化列表中初始化。
引用成员变量(reference):引用必须在创建对象时初始化,并且一旦初始化后,就不能再引用其他对象。因此,引用成员变量必须在初始化列表中初始化。
成员变量对象(member object):如果类包含其他类的对象作为成员变量,而这些成员对象没有默认构造函数或者需要传递参数进行初始化,那么它们必须在初始化列表中初始化。
以下是不能在初始化列表中初始化的情况:
静态成员变量(static):静态成员变量在类定义外进行初始化,而不是在构造函数或初始化列表中。
父类的成员变量不能在子类的初始化列表中直接进行初始化,可以在父类的初始化列表中初始化
GCC编译器的结构体对齐指令
gcc推荐的结构体对齐指令
__attribute__((packed))
__attribute__((aligned(n)))
(1) __attribute__((packed))
使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。packed的作用就是取消对齐访问。
注意:定义变量时,加 __attribute__((packed))
是不起作用的,说明__attribute__((packed))
只能加在结构体类型后面,只能影响这个结构体类型的整体自己。
(2) __attribute__((aligned(n)))
使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。它的作用是让整个结构体变量整体进行n字节对齐。
(注意__attribute__((aligned(n)))
的作用是结构体变量整体n字节对齐,而不是结构体内各元素也要n字节对齐),这句话什么意思呢?简单理解:当32位编译器,默认对齐时4字节对齐,但是你会发现一个奇怪现象,当 __attribute__((aligned(n)))
中的n=1、2、4时,他的打印结果都是12(这是4字节对齐的结果:12=4+2+2+4),因此可以看出,这里并不是要求结构体内部各元素都要按照n字节对齐,而是要求结构体整体对齐。只有当n的数值大于或等于4时(要求是2的幂次方)才会起作用。
getline
ACM 输入输出模式必学的技巧:getline 和 stringstream
getline 函数原型
template< class CharT, class Traits, class Allocator >
std::basic_istream<CharT,Traits>& getline( std::basic_istream<CharT,Traits>&& input,
std::basic_string<CharT,Traits,Allocator>& str,
CharT delim );
getline 从输入流读取字符并将它们放进 string :
表现为无格式输入函数 (UnformattedInputFunction) ,除了不影响 input.gcount() 。构造并检查 sentry 对象后,进行下列操作:
1. 调用 str.erase() 2. 从 input 释出字符并后附它们到 str ,直至发生下列条件之一(按顺序检查): 1. input 上的文件尾条件,该情况下, getline 设置 eofbit 。 2. 下个可用输入字符是 delim ,以 Traits::eq(c, delim) 测试,该情况下从 input 释出分隔字符,但不后附它到 str 。 3. 已经存储 str.max_size() 个字符,该情况下 getline 设置 failbit 并返回。 3. 若因任何原因(不是舍弃的分隔符)没有释出字符,则 getline 设置 failbit 并返回。
同 getline(input, str, input.widen(‘\n’)) ,即默认分隔符是换行符。
- 参数
input
- 获取数据来源的流str
- 放置数据的目标 stringdelim
- 分隔字符
- 返回值
input
- 注解
消耗空白符分隔的输入(例如 int n; std::cin >> n; )时,任何后随的空白符,包括换行符都会被留在流中。然后当切换到面向行的输入时,以 getline 取得的首行只会是该空白符。多数情况下这是不想要的行为,可能的解法包括:
对 getline 的显式的额外初始调用
以 std::cin >> std::ws 移除额外的空白符
以 std::cin.ignore(std::numeric_limitsstd::streamsize::max(), ‘\n’); 忽略输入行上剩下的全部字符
1 |
|
Lambda 表达式
捕获列表
上面介绍完了lambda表达式的各个成分,其实很多部分和正常的函数没什么区别,其中最大的一个不同点就是捕获列表。我在刚开始用lambda表达式的时候,还一直以为这个没啥用,只是用一个 [] 来标志着这是一个lambda表达式。后来了解了才知道,原来这个捕获列表如此强大,甚至我觉得捕获列表就是lambda表达式的灵魂。下面先介绍几种常用的捕获方式。
[] 什么也不捕获,无法lambda函数体使用任何
[=] 按值的方式捕获所有变量
[&] 按引用的方式捕获所有变量
[=, &a] 除了变量a之外,按值的方式捕获所有局部变量,变量a使用引用的方式来捕获。这里可以按引用捕获多个,例如 [=, &a, &b,&c]。这里注意,如果前面加了=,后面加的具体的参数必须以引用的方式来捕获,否则会报错。
[&, a] 除了变量a之外,按引用的方式捕获所有局部变量,变量a使用值的方式来捕获。这里后面的参数也可以多个,例如 [&, a, b, c]。这里注意,如果前面加了&,后面加的具体的参数必须以值的方式来捕获。
[a, &b] 以值的方式捕获a,引用的方式捕获b,也可以捕获多个。
[this] 在成员函数中,也可以直接捕获this指针,其实在成员函数中,[=]和[&]也会捕获this指针。
1 |
|
编译器如何看待Lambda表达式
我们把lambda表达式看成一个函数,那编译器怎么看待我们协的lambda呢?
其实,编译器会把我们写的lambda表达式翻译成一个类,并重载 operator()来实现。比如我们写一个lambda表达式为
1 |
|
那么编译器会把我们写的表达式翻译为
1 |
|
调用的时候编译器会生成一个Lambda的对象,并调用opeartor ()函数。(备注:这里的编译的翻译结果并不和真正的结果完全一致,只是把最主要的部分体现出来,其他的像类到函数指针的转换函数均省略)
上面是一种调用方式,那么如果我们写一个复杂一点的lambda表达式,表达式中的成分会如何与类的成分对应呢?我们再看一个 值捕获 例子。
1 |
|
编译器的翻译结果为
1 |
|
其实这里就可以看出,值捕获时,编译器会把捕获到的值作为类的成员变量,并且变量是以值的方式传递的。需要注意的时,如果所有的参数都是值捕获的方式,那么生成的operator()函数是const函数的,是无法修改捕获的值的,哪怕这个修改不会改变lambda表达式外部的变量,如果想要在函数内修改捕获的值,需要加上关键字 mutable。向下面这样的形式。
1 |
|
引用的方式捕获变量
1 |
|
编译器的翻译结果为
1 |
|
我们可以看到以引用的方式捕获变量,和值捕获的方式有3个不同的地方:
1. 参数引用的方式进行传递;
2. 引用捕获在函数体修改变量,会直接修改lambda表达式外部的变量;
3. opeartor()函数不是const的。
针对上面的集中情况,我们把lambda的各个成分和类的各个成分对应起来就是如下的关系:
捕获列表,对应LambdaClass类的private成员。
参数列表,对应LambdaClass类的成员函数的operator()的形参列表
mutable,对应 LambdaClass类成员函数 operator() 的const属性 ,但是只有在捕获列表捕获的参数不含有引用捕获的情况下才会生效,因为捕获列表只要包含引用捕获,那operator()函数就一定是非const函数。
返回类型,对应 LambdaClass类成员函数 operator() 的返回类型
函数体,对应 LambdaClass类成员函数 operator() 的函数体。
引用捕获和值捕获不同的一点就是,对应的成员是否为引用类型。
参考文章
inline
1、引入 inline 关键字的原因
在 c/c++ 中,为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了 inline 修饰符,表示为内联函数。
栈空间就是指放置程序的局部数据(也就是函数内数据)的内存空间。
在系统下,栈空间是有限的,假如频繁大量的使用就会造成因栈空间不足而导致程序出错的问题,如,函数的死循环递归调用的最终结果就是导致栈内存空间枯竭。
下面我们来看一个例子:
实例
1 |
|
上面的例子就是标准的内联函数的用法,使用 inline 修饰带来的好处我们表面看不出来,其实,在内部的工作就是在每个 for 循环的内部任何调用 num_check(i) 的地方都换成了 (i%2>0)?”奇”:”偶”,这样就避免了频繁调用函数对栈内存重复开辟所带来的消耗。
2、inline使用限制
inline 的使用是有所限制的,inline 只适合涵数体内代码简单的函数使用,不能包含复杂的结构控制语句例如 while、switch,并且不能内联函数本身不能是直接递归函数(即,自己内部还调用自己的函数)。
3、inline仅是一个对编译器的建议
inline 函数仅仅是一个对编译器的建议,所以最后能否真正内联,看编译器的意思,它如果认为函数不复杂,能在调用点展开,就会真正内联,并不是说声明了内联就会内联,声明内联只是一个建议而已。
4、建议 inline 函数的定义放在头文件中
其次,因为内联函数要在调用点展开,所以编译器必须随处可见内联函数的定义,要不然就成了非内联函数的调用了。所以,这要求每个调用了内联函数的文件都出现了该内联函数的定义。
因此,将内联函数的定义放在头文件里实现是合适的,省却你为每个文件实现一次的麻烦。
声明跟定义要一致:如果在每个文件里都实现一次该内联函数的话,那么,最好保证每个定义都是一样的,否则,将会引起未定义的行为。如果不是每个文件里的定义都一样,那么,编译器展开的是哪一个,那要看具体的编译器而定。所以,最好将内联函数定义放在头文件中。
5、类中的成员函数与inline
定义在类中的成员函数默认都是内联的,如果在类定义时就在类内给出函数定义,那当然最好。如果在类中未给出成员函数定义,而又想内联该函数的话,那在类外要加上 inline,否则就认为不是内联的。
1 |
|
将成员函数的定义体放在类声明之中虽然能带来书写上的方便,但不是一种良好的编程风格,上例应该改成:
1 |
|
6、inline 是一种”用于实现的关键字”
关键字 inline 必须与函数定义体放在一起才能使函数成为内联,仅将 inline 放在函数声明前面不起任何作用。
如下风格的函数 Foo 不能成为内联函数:
1 |
|
而如下风格的函数 Foo 则成为内联函数:
1 |
|
所以说,inline 是一种”用于实现的关键字”,而不是一种”用于声明的关键字”。一般地,用户可以阅读函数的声明,但是看不到函数的定义。尽管在大多数教科书中内联函数的声明、定义体前面都加了inline 关键字,但我认为inline不应该出现在函数的声明中。这个细节虽然不会影响函数的功能,但是体现了高质量C++/C 程序设计风格的一个基本原则:声明与定义不可混为一谈,用户没有必要、也不应该知道函数是否需要内联。
7、慎用 inline
内联能提高函数的执行效率,为什么不把所有的函数都定义成内联函数?如果所有的函数都是内联函数,还用得着”内联”这个关键字吗?
内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。
如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
以下情况不宜使用内联:
(1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。类的构造函数和析构函数容易让人误解成使用内联更有效。要当心构造函数和析构函数可能会隐藏一些行为,如”偷偷地”执行了基类或成员对象的构造函数和析构函数。所以不要随便地将构造函数和析构函数的定义体放在类声明中。一个好的编译器将会根据函数的定义体,自动地取消不值得的内联(这进一步说明了 inline 不应该出现在函数的声明中)。
8、总结
内联函数并不是一个增强性能的灵丹妙药。只有当函数非常短小的时候它才能得到我们想要的效果;但是,如果函数并不是很短而且在很多地方都被调用的话,那么将会使得可执行体的体积增大。
最令人烦恼的还是当编译器拒绝内联的时候。在老的实现中,结果很不尽人意,虽然在新的实现中有很大的改善,但是仍然还是不那么完善的。一些编译器能够足够的聪明来指出哪些函数可以内联哪些不能,但是大多数编译器就不那么聪明了,因此这就需要我们的经验来判断。如果内联函数不能增强性能,就避免使用它!
面向对象设计的原则
面向对象设计(Object-Oriented Design,OOD)有许多原则和准则,下面是几个广为人知的重要原则:
单一职责原则(Single Responsibility Principle,SRP):
一个类应该只有一个引起它变化的原因。换句话说,一个类应该只有一个职责。这样可以提高类的可读性、可维护性和可测试性。开放封闭原则(Open-Closed Principle,OCP):
软件实体(类、模块、函数等)应该对扩展(变化)开放,而对修改关闭。这样可以保持系统的稳定性,同时方便添加新功能或修改现有功能。里氏替换原则(Liskov Substitution Principle,LSP):
子类对象应该能够替换其父类对象,并且程序不会产生错误或异常。这意味着子类应该遵循父类所定义的行为规范,而不改变父类的预期行为。依赖倒置原则(Dependency Inversion Principle,DIP):
高层模块不应该依赖于低层模块,而是应该依赖于抽象接口。通过依赖注入等方式,可以降低模块之间的耦合度,提高系统的可维护性和可测试性。接口隔离原则(Interface Segregation Principle,ISP):
客户端不应该依赖于它不需要的接口。一个类或模块只应该依赖于其需要的接口,避免对无用接口的依赖,从而减少耦合度。迪米特法则(Law of Demeter,LoD):
一个对象应该对其他对象有尽可能少的了解,也就是最小知道原则。对象之间的耦合越低,系统越灵活、可重用性越高。
这些原则都是为了提高软件设计的质量和可维护性,通过降低耦合度、增加可复用性、提高扩展性等方面来实现优秀的面向对象设计。在实际应用中,根据具体情况和需求选择合适的原则,并结合设计模式等技术进行实践。
c++中继承的特点
C++ 中的继承是面向对象编程中的重要特性,它允许将一个类的特性和行为传递给另一个类,从而实现代码重用和组织。
C++ 中的继承具有以下几个特点:
类的单继承和多继承:C++ 支持单继承和多继承。单继承是指一个类只有一个直接的父类,而多继承则是指一个类可以同时从多个父类继承属性和方法。多继承需要注意解决命名冲突等问题。
派生类增加新功能:通过继承,派生类可以获得基类的所有属性和方法,并且可以在此基础上添加自己的新功能。派生类可以添加新的成员函数,也可以覆盖(override)基类中的成员函数。
访问控制:C++ 中的继承也遵循访问控制规则,即公有继承、保护继承和私有继承。公有继承表示基类中的公有成员在派生类中仍然是公有的,基类的保护成员仍然是保护成员;保护继承表示基类中的公有成员在派生类中变为了保护成员;私有继承表示基类中的公有和保护成员在派生类中变为了私有成员。
构造函数和析构函数:C++ 中的构造函数和析构函数也可以被继承,派生类必须调用基类的构造函数来初始化从基类继承的成员变量和方法。析构函数的调用顺序与构造函数的调用顺序相反。
虚函数和多态:C++ 中的虚函数机制可以支持运行时多态。通过在基类中声明虚函数,并在派生类中覆盖它们,可以实现不同对象之间的动态绑定(dynamic binding)。这种特性可以方便地实现基于接口设计和实现等场景。
总之,C++ 中的继承提供了一种强大的代码复用和组织机制,使得程序设计更加清晰、易于维护和扩展。但是,在使用继承时需要注意解决命名冲突、控制访问权限、避免继承深度过深等问题,以保证程序的正确性和可读性。
类多继承需要注意什么
在使用类多继承时,需要注意以下几点:
命名冲突:多继承可能导致不同父类具有相同名称的成员(属性、方法等),这会引起命名冲突。为避免冲突,需要进行适当的命名空间管理或在派生类中使用完全限定名来引用成员。
虚函数与菱形继承问题:当存在多个父类时,并且其中某些类在继承关系中共享一个基类,就会出现所谓的菱形继承问题(也称菱形继承二义性)。例如,派生类 D 继承自两个基类 B 和 C,而 B 和 C 又都继承自基类 A。如果存在与 A 相关的虚函数,在 D 类中调用该虚函数可能会产生歧义。可以通过虚拟继承(virtual inheritance)来解决该问题,即在 B 和 C 继承 A 的地方使用 virtual 关键字。
父类构造函数调用:当一个类有多个父类时,需要确保每个父类的构造函数被正确调用来初始化它们的成员。在派生类的构造函数中,需要显式调用每个父类的构造函数,并按照正确的顺序传递参数。
类的设计复杂性:多继承可能引入更大的设计复杂性,增加代码的理解和维护难度。因此,在使用多继承时应慎重考虑,并遵循良好的设计原则,如单一职责原则、接口隔离原则等,以确保代码的可读性和可维护性。
合理使用多继承:多继承在某些情况下可以提供更灵活的设计方案,但也容易导致过度使用。应该评估是否真正需要多继承,以及是否可以通过其他方式(如组合、接口等)来达到相同的效果。
总之,使用类多继承时需要注意处理命名冲突、避免菱形继承问题、正确调用父类构造函数,并合理设计和使用多继承,以确保代码的正确性、清晰性和可维护性。
c++子类继承父类的过程中那些方法和成员变量不会被继承
在 C++ 中,子类继承父类的过程中有一些方法和成员变量不会被继承,主要包括以下几种情况:
父类的构造函数和析构函数:子类虽然可以继承父类的构造函数和析构函数,但并不是所有的都会被继承。例如,若父类中存在 private 型构造函数,则子类在继承时并不能直接调用该构造函数。
父类的赋值运算符(赋值操作符=): 子类继承父类时,父类中的赋值运算符不会被自动继承,需要手动定义子类中的赋值运算符。
父类中的 static 成员变量和静态方法:在子类中,static 成员变量和静态方法是属于父类并不会被子类继承。如果需要在子类中使用父类的 static 成员和静态方法,需要使用完全限定名来引用父类的成员。(但是在子类中是可以直接访问静态变量和静态方法的)
父类中的私有成员:由于私有成员只能被类本身访问,其他类不能访问私有成员。因此,子类无法继承父类中的私有成员,子类只能通过父类提供的公有接口或者受保护接口访问其私有成员。
父类的友元关系:子类一般不会继承父类中的友元关系,因为友元关系是建立在类之间的而非继承关系上。如果需要在子类中维护父类的友元关系,需要在子类中重新声明和定义相关的函数,并将其声明为友元函数。
总之,子类继承父类时,虽然可以继承很多东西,但一些特殊的成员变量和方法并不会被自动继承。为了正确地继承和使用父类的成员,需要注意这些情况并适当地处理。
emplace_back和push_back
在C++中,emplace_back和push_back都是用于向容器(如std::vector)尾部插入元素的成员函数。然而,在插入pair类型时,它们的行为有一些区别。
push_back函数接受一个对象作为参数,将其副本插入容器中。当使用push_back插入一个pair对象时,会进行一次复制操作,将该pair对象的副本插入容器。
1 |
|
相比之下,emplace_back函数可以直接在容器尾部构造元素,避免了复制的开销。它接受构造元素所需的参数,并在容器内部直接构造对象。
1 |
|
使用emplace_back时,参数被传递到pair的构造函数中,直接在容器内部构造一个新的pair对象,避免了中间的复制步骤。
总结起来,push_back用于将已有的对象副本插入容器,而emplace_back则直接在容器内构造对象,避免了复制的开销。因此,在插入pair类型时,emplace_back通常更高效
此外C++17 中引入的结构化绑定语法,将 spaces[0] 中的两个 int 值分别绑定到变量 i 和 j 上:
1 |
|
这相当于以下代码:
1 |
|