01.让自己习惯C++

让自己习惯C++

条款1:视C++为一个语言联邦

条款1中提到了将C++看作为一个“语言联邦”的概念。具体来说,“语言联邦”是指将C++看作由多种不同的子语言组成的联邦。每种子语言都有自己的惯用法、工具和库,可以用来解决特定的问题。因此,C++程序员应该了解这些子语言,并选择最适合解决特定问题的子语言。 这个概念的含义可以从以下几个方面来解释:

  1. 多种子语言:C++由多种子语言组成,每种子语言都有自己的规则、习惯和约定。例如,面向对象编程(OOP)子语言、模板元编程(TMP)子语言等。这些子语言都有自己的语法和语义,可以用来解决特定的问题。
  2. 惯用法和工具:每种子语言都有自己的惯用法和工具。例如,OOP中常用的类、继承、多态等概念,以及与之对应的工具和库如STL、Boost等。熟悉这些惯用法和工具可以提高程序员的开发效率和代码质量。
  3. 选择最适合的子语言:C++程序员应该了解这些子语言,并选择最适合解决特定问题的子语言。例如,如果要处理大量的数值计算,可以选择使用TMP子语言中的模板元编程技术,来提高程序的性能如果要实现一些复杂的数据结构和算法,可以使用STL等库来简化代码

总的来说,可以将C++视为一个由4个次语言组成的联邦而非单一语言:

  1. C,说到底C++仍是以C为基础。
  2. object-oriented C++,包括封装、继承、多态等面向对象设计。
  3. template C++,泛型编程,衍生出模板元编程(在各个新标准中逐步完善)。
  4. STL,包括容器、迭代器、算法与函数对象。

条款2:尽量以const、enum、inline替换#define

条款2中提到了尽量使用constenuminline来替代#define的概念。具体来说,#define是一种预处理指令,可以将一个标识符定义为一个值或一个字符串。而constenuminline都是C++语言中的关键字,也可以用于定义常量和函数。以下是对这个条款的一些解释:

  1. #define的缺点:使用#define定义常量存在一些缺点,例如它不会进行类型检查,容易引起意外的副作用,也不会被语法检查工具正确地处理
  2. const的优点:使用const定义常量可以避免#define的缺点。const定义的常量有类型,可以被编译器检查和优化,也可以被调试器和其他工具正确地处理。
  3. enum的优点:如果要定义一系列相关的常量,可以使用enum枚举类型。enum定义的常量有类型,可以被编译器检查和优化,同时也可以提高代码的可读性和可维护性。
  4. inline的优点:如果要定义一个简单的函数或者函数模板,可以使用inline关键字。inline函数在编译时会被展开,从而避免了函数调用的开销。同时,inline函数也可以提高代码的可读性和可维护性。

#define可以用来定义一些变量、函数,但它只是一方面单纯的文本替换,并且没有任何类型检查,导致容易引起莫名其妙的问题,另一方面预处理后已经消失,编译链接过程中没有其符号信息,出问题时无法定位到它。

#define CALL_WITH_MAX(a,b) f((a)>(b)?(a):(b))int a=5,b=0;
CALL_WITH_MAX(++a,b);
CALL_WITH_MAX(++a,b+10);

即使上文的宏已经仔细地为所有参数添加小括号,仍然出现了问题:第一次调用中a被累加两次,第二次调用中a被累加一次。

CALL_WITH_MAX(++a,b)在展开后变成了f((++a)>(b)?(++a):(b)),其中a的值被多次递增。

CALL_WITH_MAX(++a,b+10)在展开后变成了f((++a)>(b+10)?(++a):(b+10)),其中a的值也被多次递增。

因此,这两个调用会导致a的值被递增多次,结果可能不是我们所期望的。

为了避免这个问题,更加可预测并且类型安全的写法是,可以使用函数模板来替代#define宏。使用const对象(对于一系列常量,使用枚举或枚举类,而不是一系列#define)

例如,可以定义一个template<typename T> inline void callWithMax(const T& a, const T& b)函数来替代CALL_WITH_MAX宏。

这样做不仅可以避免上述问题,还可以提高代码的可读性和可维护性。例如:

template<typename T>
inline void callWithMax(const T& a, const T& b)
{
f(a > b ? a : b);
}
int a = 5, b = 0;
callWithMax(++a, b);
callWithMax(++a, b + 10);
// 这段代码中,a的值只会被递增一次,符合我们的预期。
// 不过inline目前主要指多重定义而非内联

条款3:尽可能使用const

条款3中提到了尽可能使用const的概念。具体来说,const是C++语言中的关键字,用于定义常量。以下是对这个条款的一些解释:

  1. const的作用:使用const可以将变量定义为常量,即不能被修改。常量可以提高代码的可读性和可维护性,同时也可以避免意外的修改导致的错误。
  2. const的使用场景:在C++中,const可以用于定义常量、函数参数和函数返回值等。使用const定义常量时,可以使用const关键字加上变量的类型,例如const int MAX_SIZE = 1024;。使用const定义函数参数时,可以在参数类型前加上const关键字,例如void foo(const std::string& str);。使用const定义函数返回值时,可以在函数声明和定义中返回类型前加上const关键字,例如const std::string& foo() const;
  3. const的作用域:在C++中,const变量和const函数的作用域与普通变量和函数的作用域相同。如果const变量或const函数在某个作用域内定义,那么它们只能在该作用域内使用。
  4. const和指针:使用指针时,const可以用于限定指针本身或指针所指向的内容是否可修改。例如,const int* p表示指向const int类型的指针,即指针所指向的内容不能被修改;int* const p表示指向int类型的const指针,即指针本身不能被修改。另外,const还可以同时限定指针本身和指针所指向的内容是否可修改,例如const int* const p表示指向const int类型的const指针。
char greeting[] = "Hello";
char* p1 = greeting;
const char* p2 = greeting;       //被指物不可修改
char* const p3 = greeting        //指针不可修改
const char* const p4 = greeting; //皆不可修改

真正威力强大的用法是面对函数声明时,const可以和函数返回值、各参数、成员函数自身产生关联。例如令函数返回const,往往可以降低因用户错误而造成的意外,又不至于放弃安全性和高效性。

class Rational{...};
const Rational operator*(const Rational& lhs,const Rational& rhs);
//上述写法可以避免用户写出 a*b = c

对于成员函数自身的const,编译器强制实施bitwise const,即强制不能修改任何成员变量。**这意味着,在const成员函数中,即使我们使用了mutable关键字,也不能修改任何非mutable成员变量。**但实际上很多情况下我们需要的是logical const,即const成员函数也应该可以修改某些客户不可见的数据,这时可以用mutable成员变量来绕过const成员函数的限制。

例如对于一个文本块的对象而言,其内部很可能存在高速缓存;对于查询文本块长度这样的const操作,仍然需要更新高速缓存:

class TextBlock{
public:std::size_t length() const;
private:char *pText;mutable std::size_t text_length;mutable bool length_is_valid;
};std::size_t TextBlock::length() const{if(!length_is_valid){text_length = std::strlen(pText);length_is_valid = true;}return text_length;
};

这是一个名为TextBlock的类,其中包含一个私有成员变量char *pText,表示一个C风格的字符串。该类还包含了两个mutable类型的私有成员变量std::size_t text_lengthbool length_is_valid,用于缓存字符串长度和标记长度是否已经被计算。该类还定义了一个公有成员函数std::size_t length() const,用于获取字符串的长度。下面是对该类的解释:

  1. TextBlock类中的char *pText表示一个C风格的字符串,但是没有提供构造函数或析构函数来管理字符串的内存,这样会存在内存泄漏的风险,需要在类中添加构造函数和析构函数来管理字符串的内存。
  2. **TextBlock类中的text_lengthlength_is_valid成员变量被声明为mutable类型,表示即使在const函数中也可以被修改。**这是因为length()函数需要计算字符串的长度,如果多次调用该函数,每次都重新计算字符串长度会浪费时间,因此使用mutable类型的成员变量缓存计算结果,避免重复计算。
  3. TextBlock类中的length()函数是一个const函数,表示该函数不会修改类的成员变量,因此可以在const对象中调用。在函数中使用!length_is_valid判断是否需要重新计算字符串长度,如果需要计算,则调用std::strlen(pText)计算字符串长度,再将计算结果缓存到text_length中,并将length_is_valid标记为true。最后,返回缓存的字符串长度。
  4. 由于TextBlock类中的pText变量是一个C风格的字符串,并且没有提供构造函数和析构函数来管理内存,因此在使用该类时需要特别注意内存泄漏的问题。可以通过使用std::string等C++标准库提供的字符串类型来避免这个问题。

C++中两个函数如果只是常量性不同,也可以重载。当const成员函数与非const成员函数有着实质等价的实现时,为了避免冗余,可以令non-const版本调用const版本:

class TextBlock{
public:const char& operator[](std::size_t position) const{...}char& operator[](std::size_t position){return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);}
};
//后者首先将自身转换为const对象
//随后调用const成员函数,返回const引用
//最后转换为non-const引用

这是一个名为TextBlock的类,其中定义了两个下标运算符operator[],用于访问类中的字符数据。第一个下标运算符是一个const成员函数,返回一个const char&类型的引用,表示在指定位置的字符数据。第二个下标运算符是一个非const成员函数,返回一个char&类型的引用,表示在指定位置的字符数据。下面是对该代码的解释:

  1. 第一个下标运算符是一个const成员函数,表示该函数不会修改类的成员变量。在函数中,使用const关键字修饰函数的返回值,表示返回的是一个常量引用。该函数返回指定位置的字符数据,并且由于返回的是一个常量引用,因此客户端无法通过该函数修改类的成员变量。
  2. 第二个下标运算符是一个非const成员函数,表示该函数可以修改类的成员变量。在函数中,我们使用了const_caststatic_cast两个操作符,将该函数转换成了一个const成员函数的调用。具体来说,我们首先使用static_cast<const TextBlock&>(*this)将当前对象转换成一个const TextBlock&类型的对象,然后调用第一个下标运算符,返回在指定位置的字符数据的常量引用。接着,我们使用const_cast<char&>将常量引用转换为一个非常量引用,从而使得客户端可以通过该函数修改类的成员变量。需要注意的是,这种使用const_cast的方式是有风险的,因为它可以绕过函数的const限制,可能导致程序的未定义行为和内存安全问题。
  3. 如果我们希望在第二个下标运算符中修改一些客户端不可见的数据,可以使用mutable关键字修饰一个成员变量,避免使用const_cast绕过const限制。

条款4:确定对象被使用前已先被初始化

条款4的意思是,在使用一个对象之前,必须确保该对象已经被正确地初始化。如果一个对象没有被正确地初始化,那么它的行为是未定义的,可能会导致程序崩溃、数据损坏等不可预测的结果。 在C++中,对象的初始化方式有多种,包括默认初始化、值初始化、直接初始化、拷贝初始化等。不同的初始化方式会对对象的状态产生不同的影响。为了保证对象被正确地初始化,我们应该遵循以下几个原则:

  1. **明确对象的初始化方式。**在定义对象时,应该清楚地指定对象的初始化方式,避免使用未初始化的对象。
  2. **尽可能使用构造函数进行初始化。**构造函数是一种专门用于初始化对象的函数,可以保证对象的状态正确。因此,在定义对象时,应该尽可能使用构造函数进行初始化。
  3. **避免使用未定义的对象。**在使用对象之前,应该确保对象已经被正确地初始化。如果不确定对象是否已经被初始化,就应该避免使用该对象。
  4. **避免使用未定义的成员变量。**在定义类时,应该确保类的成员变量都被正确地初始化。如果一个成员变量没有被正确地初始化,那么该成员变量的行为也是未定义的,可能会导致程序崩溃、数据损坏等不可预测的结果。

C++中变量并非一定会进行初始化。最佳处理办法是:对于内置类型必须手动初始化,而对于用户定义的对象,在使用对象前将其初始化(责任落在构造函数上)。

构造函数包含成员初值列与函数体。

  1. 最好使用成员初始列的初始化而非函数体内的赋值,否则对象会在成员初始列的步骤中进行默认初始化,再在赋值的过程中进行拷贝,成本增高。
  2. 成员初始列的排列顺序应与在类中的声明次序一致,因为成员初始化顺序只与后者有关,前者若与后者不一致的话可能导致误解。

只剩最后一个难点:函数内的静态变量称为local静态变量,其他的都是non-local;而不同编译单元(一个编译单元指产出单一目标文件的源码们)内定义的non-local静态对象的初始化顺序并未规定。倘若存在这样的两个变量a和b,且b的初始化需要使用a,如果a尚未初始化就被b使用了,显然程序会出错。

local静态变量指的是函数内定义的静态变量,只在函数的作用域内可见;

而non-local静态变量指的是在全局作用域或命名空间内定义的静态变量,可以被多个函数使用。

在C++中,对于non-local静态变量的初始化顺序并没有严格的规定。这意味着,如果存在两个non-local静态变量a和b,且b的初始化需要使用a,那么如果a尚未初始化就被b使用了,就会导致程序出错。这是因为,如果a尚未初始化,那么它的值是不确定的,可能是一个随机值,也可能是0或其他默认值。如果b在使用a之前被初始化,那么它使用的a的值是不确定的,这可能会导致程序出错。

为了避免这种问题,我们可以采用一些编程技巧和约定来确保non-local静态变量的正确初始化顺序。例如,可以使用单例模式等设计模式来确保对象的初始化顺序;或者可以将non-local静态变量的初始化工作放在函数内部,以确保它们在第一次使用之前被正确地初始化。此外,我们还可以使用编译器提供的一些选项来控制non-local静态变量的初始化顺序,但这种方法并不是跨平台的,可能会导致代码的可移植性问题。

解决方法也很简单:将每个non-local静态变量移到自己的专属函数内,这些函数返回该静态变量的引用,用户使用这些函数而非直接使用变量(类似单例模式)。至此,non-local静态变量被local静态变量取代。

class FileSystem{...};
FileSystem& tfs(){static FileSystem fs;return fs;
}
class Directory{...};
Directory::Directory(...){...std::size_t disks = tfs().num_disks();...
}

这段代码定义了一个名为FileSystem的类和一个名为tfs()的函数,以及另一个名为Directory的类和它的一个构造函数。

tfs()函数内部,定义了一个名为fs的静态对象,它是FileSystem类的一个实例,并且返回了这个静态对象的引用。

Directory类的构造函数内部,首先执行了一些初始化工作,然后通过调用tfs()函数获取到了FileSystem对象的引用,并通过该引用调用了num_disks()函数,将返回值存储在了一个名为disks的变量中。

由于tfs()函数内部定义的fs对象是一个静态对象,因此它在程序运行期间只会被创建一次,并且在整个程序的生命周期内都存在。

每次调用tfs()函数时,都会返回同一个静态对象的引用。这种方式可以保证FileSystem类的实例只有一个,并且可以在全局范围内被访问。

Directory类的构造函数内部,通过调用tfs()函数获取到了FileSystem对象的引用,并且调用了它的num_disks()函数,这种方式可以确保Directory类的实例可以访问到全局唯一的FileSystem对象,并且可以获取到该对象的属性和方法。

总之,这段代码通过使用静态变量和函数,实现了一个全局唯一的FileSystem对象,并且可以在其他类的构造函数中使用该对象,从而避免了对象的多次创建和初始化,提高了程序的效率和可读性。同时,该代码还展示了C++中静态变量和静态函数的用法,可以作为学习C++语言的参考。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/169715.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

『亚马逊云科技产品测评』活动征文|低成本搭建物联网服务器thingsboard

授权声明&#xff1a;本篇文章授权活动官方亚马逊云科技文章转发、改写权&#xff0c;包括不限于在 Developer Centre, 知乎&#xff0c;自媒体平台&#xff0c;第三方开发者媒体等亚马逊云科技官方渠道。 0. 环境 - ubuntu22&#xff08;注意4G内存勉强够&#xff0c;部署完…

『Postman入门万字长文』| 从工具简介、环境部署、脚本应用、Collections使用到接口自动化测试详细过程

『Postman入门万字长文』| 从工具简介、环境部署、脚本应用、Collections使用到接口自动化测试详细过程 1 Postman工具简介2 Postman安装3 Postman界面说明4 一个简单请求4.1 请求示例4.2 请求过程 5 Postman其他操作5.1 import5.2 History5.3 Environment5.4 Global5.5 其他变…

使用信息面板沟通研发工作

凌鲨里面的内容面板里面有专门针对研发团队的白板功能&#xff0c;它可以把文档&#xff0c;图片&#xff0c;软件设计&#xff0c;需求&#xff0c;任务/缺陷等相关研发要素串接起来。 使用 你还可以调整背景颜色。 引用项目内数据 点击面板中的连接会在右侧打开对应内容

不要再往下翻了,你要的女宝穿搭我都有哦

分享女儿的睡衣穿搭 清新自然的浪漫紫 一眼就击中了我的心巴 软糯亲肤上身体验感超赞 轻松自在无束缚 防风又保暖&#xff0c;居家外出都可哦

SpringBoot实现文件批量打包下载

实现将指定的多个文件打包成一个压缩文件下载。 1. 引入pom依赖 <dependencies><!-- Spring Web --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency&…

Angular中的getter函数

Angular 中的 getter 函数每次被调用时会返回一个新对象时&#xff0c;这些新对象并不使用同一个堆内存。详细解释一下&#xff1a; Getter 函数的作用是获取某个属性的值。在 Angular 中&#xff0c;getter 函数通常用于获取响应式数据&#xff08;例如 Observables 或 Signal…

基于C#实现Kruskal算法

这篇我们看看第二种生成树的 Kruskal 算法&#xff0c;这个算法的魅力在于我们可以打一下算法和数据结构的组合拳&#xff0c;很有意思的。 一、思想 若存在 M{0,1,2,3,4,5}这样 6 个节点&#xff0c;我们知道 Prim 算法构建生成树是从”顶点”这个角度来思考的&#xff0c;然…

# Panda3d 碰撞检测系统介绍

Panda3d 碰撞检测系统介绍 文章目录 Panda3d 碰撞检测系统介绍碰撞几何体的介绍碰撞球体碰撞胶囊反碰撞球体碰撞平面碰撞多边形碰撞射线碰撞直线碰撞段碰撞抛物线碰撞长方体碰撞系统图碰撞处理器碰撞处理器队列碰撞处理器事件碰撞处理器回退模型(CollisionHandlerPusher)物理…

ArkTS基础知识 【习题】

判断题 1.循环渲染ForEach可以从数据源中迭代获取数据&#xff0c;并为每个数组项创建相应的组件。 正确(True) 2. Link变量不能在组件内部进行初始化。 正确(True) 单选题 1.用哪一种装饰器修饰的struct表示该结构体具有组件化能力&#xff1f;(A) A. Component B. Entry C…

c语言内存管理

通常程序访问的是虚拟内存&#xff0c;虚拟内存映射到物理内存的一小部分。 在Linux系统中&#xff0c;虚拟内存默认为4G的大小。每个进程都有独立的4G内存地址空间。 int main() {char s[] "hello world"; //s数组位于栈区&#xff0c;复制了一份字符串到数组里ch…

【设计模式-2.1】创建型——单例模式

说明&#xff1a;设计模式根据用途分为创建型、结构性和行为型。创建型模式主要用于描述如何创建对象&#xff0c;本文介绍创建型中的单例模式。 饿汉式单例 单例模式是比较常见的一种设计模式&#xff0c;旨在确保对象的唯一性&#xff0c;什么时候去使用这个对象都是同一个…

MySQL 批量插入记录报 Error 1390 (HY000)

文章目录 1.背景2.问题3.分批插入4.一次最多能插入多少条记录&#xff1f;参考文献 1.背景 Golang 后台服务使用 GORM 实现与 MySQL 的交互&#xff0c;在实现一个通过 Excel 导入数据的接口时&#xff0c;使用 Save 方法一次性插入大量记录&#xff08;>1w&#xff09;时报…

springsecurity6配置四

一、springsecurity自定义过滤url配置 package com.school.information.config;import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component;import java.util.List;/*** 需要放行的…

LiveGBS流媒体平台GB/T28181功能-查看国标设备会话列表直播会话、回放会话、下载会话、对讲会话

LiveGBS流媒体平台GB/T28181功能-查看国标设备会话列表直播会话、回放会话、下载会话、对讲会话 1、会话列表2、会话类型3、搭建GB28181视频直播平台 1、会话列表 LiveGBS-> 国标设备-》点击在线状态 点击会话列表 2、会话类型 下拉会话类型可以看到 直播会话、回放会话、…

不用排队升级GPT/获取api

想要在国内获取api key&#xff0c;可以使用这种方法 小技巧&#xff1a;目前GPT还是排队订阅&#xff0c;可以直接用链接&#xff1a;https://chat.openai.com/invite/accepted 即可跳过排队环节 接下来先看一下如何购买腾讯云服务器 第一步&#xff1a;打开腾讯云 腾讯云 …

Python爬虫知识储备

Python爬虫知识储备 一、基础知识 常见的Python爬虫相关库和工程化爬虫框架&#xff1a; 请求库&#xff1a; requests&#xff1a;用于发送HTTP请求并获取响应的流行库。它简单易用&#xff0c;适合大多数爬虫任务。urllib&#xff1a;Python的标准库之一&#xff0c;包含…

文件上传漏洞的理解

文件上传漏洞的理解 1. 漏洞描述&#xff1a; 文件上传漏洞是指攻击者可以利用Web应用程序中存在的缺陷&#xff0c;向服务器上传恶意文件&#xff0c;从而实施攻击或者获取敏感信息的安全漏洞 2. 漏洞原理&#xff1a; 文件上传漏洞通常是由于开发者在设计Web用于程序时未对上…

Visual Studio 2019 C# System.BadImageFormatException 解决方法

文章目录 1.DLL文件缺失或不匹配原因解决方法 2.系统环境变量Path下内容过多原因解决方法 3.位数错误原因解决方法 分析几种可能因素 1.DLL文件缺失或不匹配 原因 检查对应Debug路径下的DLL文件是否有缺失 解决方法 将对应的DLL文件放到Debug文件夹里面&#xff0c;检查冗余…

用结构体实现时间换算

【问题描述】用结构体类型表示时间内容&#xff08;时间以时分秒表示&#xff09;输入一个时间数据&#xff0c;在输入一个秒数n(n<60)&#xff0c;以h:m:s的形式输出过了n秒后的时间。&#xff08;超过24点以0点开始&#xff09; 【输入形式】输入的时间必须是以"时:分…

二进制搭建以太坊2.0节点-2023最新详细版文档

文章目录 一、配置 JWT 认证二、部署执行节点geth2.1 下载geth二进制文件2.2 geth节点启动三、部署共识节点Prysm3.1 下载Prysm脚本3.2 Prysm容器生成四、检查节点是否同步完成4.1 检查geth执行节点4.2 检查prysm共识节点4.3 geth常用命令五、节点同步详细说明5.1 启动时日志5.…