随着https的不断普及,目前几乎所有的网站都开启了https,但是有时候网站内的部分资源还是http的,此时因为浏览器的安全策略,浏览器默认会拦截这些资源。导致网站无法正常显示,出现报错:

Mixed Content: The page at 'https://www.xxx.com/' was loaded over HTTPS, but requested an insecure stylesheet 'http://www.xxx.com/xxx.css'. This request has been blocked; the content must be served over HTTPS.

解决方案

http协议中提供了一个头部:

Content-Security-Policy: upgrade-insecure-requests

当它被设置的时候,浏览器会自动将https网站内部的http资源请求升级为https,避免出现mixed content问题。

nginx配置方式:

add_header Content-Security-Policy upgrade-insecure-requests;

参考:Mixed Content

使用Dockerfile构建完镜像后,发现镜像特别大:

[cherry@k8s-yasuo:~]$ docker image ls 
REPOSITORY                                TAG                  IMAGE ID       CREATED             SIZE
php-fpm                                   7.4.0-alpine-ext     d9c182c6c1ae   18 minutes ago      447MB

这是一个基于alpine构建的php-fpm镜像,构建前镜像大小是90M,被我魔改后竟然达到了450M,简直不可思议。不觉明厉之下,决定研究一下为什么镜像达到了这么大。

使用docker history image可以查看镜像的每一层的构建大小:

最上面的三条就是我新增的三个RUN指令生成出来的层,可以看到,2b5947710812这个层是占用了350M,因此可以断定这个指令执行有问题。这个指令执行的是更新alpine的源然后安装上一些php的库:

RUN set -eux; \
    echo "https://mirrors.tencent.com/alpine/v3.14/community/" >>/etc/apk/repositories; \
    apk add --no-cache ghostscript imagemagick; \
    apk add --no-cache --virtual .build-deps $PHPIZE_DEPS \
        freetype-dev imagemagick-dev libjpeg-turbo-dev libpng-dev libzip-dev;

本来我期望是在这个RUN执行完成之后再执行一条apk del的命令来删除新增的库:

RUN apk del --no-network .build-deps

但是我忽略了一个问题,docker构建镜像时,每个RUN指令都会生成一个层,后面的指令生成的层是基于上个层来的。也就是说,即使第二个RUN指令删除上个RUN指令添加的文件,此时依旧还是包含了上一个层生成的数据。

解决办法:把两个RUN指令合成为一个。

alpine无法安装imagemagick问题

使用alpine的docker镜像安装imagemagick时报错:

bash-5.1# apk add imagemagick
fetch https://mirrors.tencent.com/alpine/v3.14/main/x86_64/APKINDEX.tar.gz
ERROR: unable to select packages:
  imagemagick (no such package):
    required by: world[imagemagick]

问题原因:imagemagick属于community包,仓库源中没有配置community的源。

解决方案:在/etc/apk/repositories中添加community源,如:

https://mirrors.tencent.com/alpine/v3.14/community/

关闭服务:

systemctl stop docker.service
systemctl stop docker.socket

修改/usr/lib/systemd/system/docker.socket文件:

[Socket]
ListenStream=/var/run/dockershim.sock

重新加载配置:

systemctl daemon-reload

启动服务:

systemctl start docker.socket
systemctl start docker.service

修改环境变量,在/etc/profile中添加:

export DOCKER_HOST="unix:///var/run/dockershim.sock"

如果不修改环境变量,执行docker命令时会报错无法连接上docker,因为socket路径不对:

Client:
 Context:    default
 Debug Mode: false
 Plugins:
  app: Docker App (Docker Inc., v0.9.1-beta3)
  buildx: Build with BuildKit (Docker Inc., v0.5.1-docker)
  scan: Docker Scan (Docker Inc., v0.8.0)

Server:
ERROR: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
errors pretty printing info

使用虚拟机安装CentOS5后,系统没有自动识别网口信息,导致无法配置IP远程登陆。

问题原因:

安装系统的时候,如果选择了系统是CentOS5或者更老的版本,虚拟机会将虚拟网卡设备设置为vmxnet3系列,该系列的网卡为AMD PCNet型号。但当前虚拟机的网卡实际为Intel的网卡,需要使用e1000系列网卡。两者驱动不匹配,系统识别不出来。

解决方案:

先关闭虚拟机。然后虚拟机磁盘的同目录下,找到一个名字为“虚拟机名字.vmx”的文件,是一个1kb左右的文本文件。下载下来修改其中的内容为:

ethernet0.virtualDev = "e1000"

然后再重启虚拟机即可。

centos5已停止了维护,国内大部分的源也停止维护,找了很久都没有找到可用的源。最后才发现,centos早期的源都已经vault.centos.org网站下,国内的镜像源也都改成了centos-vault,不能直接使用centos目录下的源。

腾讯云源:

[base]
name=CentOS-5.8 - Base
#mirrorlist=http://mirrorlist.centos.org/?release=5.8&arch=$basearch&repo=os
baseurl=http://mirrors.cloud.tencent.com/centos-vault/5.8/os/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-5

#released updates 
[updates]
name=CentOS-5.8 - Updates
#mirrorlist=http://mirrorlist.centos.org/?release=5.8&arch=$basearch&repo=updates
baseurl=http://mirrors.cloud.tencent.com/centos-vault/5.8/updates/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-5

#additional packages that may be useful
[extras]
name=CentOS-5.8 - Extras
#mirrorlist=http://mirrorlist.centos.org/?release=5.8&arch=$basearch&repo=extras
baseurl=http://mirrors.cloud.tencent.com/centos-vault/5.8/extras/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-5

#additional packages that extend functionality of existing packages
[centosplus]
name=CentOS-5.8 - Plus
#mirrorlist=http://mirrorlist.centos.org/?release=5.8&arch=$basearch&repo=centosplus
baseurl=http://mirrors.cloud.tencent.com/centos-vault/5.8/centosplus/$basearch/
gpgcheck=1
enabled=0
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-5

#contrib - packages by Centos Users
[contrib]
name=CentOS-5.8 - Contrib
#mirrorlist=http://mirrorlist.centos.org/?release=5.8&arch=$basearch&repo=contrib
baseurl=http://mirrors.cloud.tencent.com/centos-vault/5.8/contrib/$basearch/
gpgcheck=1
enabled=0
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-5

一、文件流

C++的IO类中定义了三个文件读写流fstream、ifstream以及ofstream,它们都继承于相同的父类istream,通过不同的实现以实现不同的文件流操作。

三者的区别为:

  • ifstream:从文件读取数据
  • ofstream:从文件写入数据
  • fstream:既可以读数据、又可以写数据

1.1 IO接口和读写模式

三个文件流实现了以下几个函数接口:

函数名用途
open(s, mode)以mode模式打开文件s
close()关闭文件流
is_open()返回文件是否已经打开
read(buff, size)读入最多size字节数据到buff中
write(buff, size)写入size字节数据到文件中

在使用open的时候,可以只传入文件s,不指定打开模式。如果不指定模式,系统会自动根据文件类型选择默认的打开模式。同时,除了open()的方式打开文件以外,还可以在对象构造的时候打开文件:

ofstream output("/tmp/test.txt", ios::out);

ios::out表示已只读方式打开文件,对应unix c中的O_WRONLY模式。在C++中,有以下读写模式可以选择:

模式说明
ios::in以读方式打开
ios::out以写方式打开
ios::app以追加写方式打开
ios::trunc以截断方式打开文件
ios:binary以二进制方式打开文件
ios::ate打开文件后指针定位到文件尾

这些模式可以单独使用,也可以组合使用,如果需要组合使用,使用逻辑操作符|或起来即可。这里要特别注意的是ios::out模式默认会截断文件,也就是说,ios::outios::out | ios::trunc效果是一样的,都会将文件截断。如果不希望以截断方式打开文件时,则需要设置读写模式为ios::out | ios::app,以这种模式打开文件后,数据会以追加的方式写入到文件。

1.2 读写文件示例

写文件

// 写文件
void write_file() {
  ofstream output;
  // 待写入数据
  string output_data = "HelloWorld\nWelcome To Tencent\n";

  // 打开文件
  output.open("test.txt", ios::out);

  // 写入数据
  output.write(output_data.c_str(), output_data.size());

  // 关闭文件
  output.close();
}

读文件

void read_file() {
  ifstream input;
  char input_data[1024];

  // 打开文件
  input.open("test.txt", ios::in);

  // 读取数据
  input.read(input_data, 1024);

  // 打印读取到的数据
  cout << input_data;

  // 关闭文件
  input.close();
}

打印结果:

1.3 以IO操作符读写数据

三个IO操作类都继承于istream,可以直接使用IO操作符(<<>>)来进行文件读写。

ofstream output("test.txt");
ifstream input("test.txt");
std::string input_data;

output << "HelloWorld\n";
output.close();

input >> input_data;
cout << input_data;
input.close();

return 0;

注意:在使用IO操作符从文件读取数据的时候,数据输入的对象可以是字符串,也可以是对应的数据类型(如int)。如果输入到字符串,默认是读到空白字符的时候就停止了,需要通过循环控制读取后面的数据。

二、流状态

流在执行IO操作的时候,会根据不同的情况产生不同的状态码,如:

状态位说明
strm::badbit流已崩溃
strm::failgitIO已崩溃
strm::eofbit已经读到文件尾
strm::goodbit一切正常,没有异常

我们可以使用已经封装好的函数bad()/fail()/eof()/good()来判断当前IO是否已经达到某种状态,如判断文件是否已经读到结束。同时,我们还可以使用clear()函数来清除当前IO对象的状态位,当流已崩溃(如将流中的字符串对象读到一个int对象上)时,可以手动来清除状态位继续往下读取。

示例,使用eof标志位判断文件是否读完成

ofstream output("test.txt");
ifstream input("test.txt");
std::string input_data;

output << "Hello World\n";
output.close();

while (!input.eof()) {  // 没有读到文件尾就一直读取
  input_data.clear();

  input >> input_data;  // 读到空白符就停止
  // 过滤掉空行
  if (input_data == "") continue;

  cout << input_data << endl;
}
input.close();

因为使用IO操作符从流中读取数据默认遇到空行就停止,所以Hello World\n字符串需要读三次才能读完,第一次是Hello,第二次是World,第三次是换行后的空行,所以,最后的输出结果是:

三、getline一次读一行

流对象中内置了getline()成员方法可以一次读一行数据到字符串数组中,需要传入一个字符数组和最大长度size:

  ifstream input("test.txt", ios::in);
  char buff[1024];
  while (!input.eof()) {
    input.getline(buff, 1024);
    cout << buff << endl;
  }

这个成员方法可以实现一次读一行的操作,但是只支持C风格的字符串传入,无法支持C++中的string类型。这里可以使用系统库的getline()函数来实现,系统库中有一个getline()函数可以直接从IO流中读取一行数据到string中:

ifstream input("test.txt", ios::in);
std::string input_data;
while (!input.eof()) {
  getline(input, input_data);
  cout << input_data << endl;
}

上面两个实现都可以正常按行输出文件内容:

四、fstream和文件指针

当使用fstream流时,对象既可以写入文件,也可以读取文件。此时读写文件指针是公共的,写入文件会导致指针后移,再读就会从当前位置重新读取(读到垃圾数据)。为了避免这个问题,可以使用seekp和seekg来移动文件指针:

fstream file("test.txt", ios::in | ios::out);
string input_data;

file << "HelloWorld\n" << unitbuf;

// 移动读指针到文件开始处
file.seekg(0, ios::beg);
file >> input_data;
cout << input_data

五、错误处理

5.1 读取文件失败了,应该如何判断

执行完open操作后,直接判断对象是否为真即可确定打开文件是否失败。出现错误后,可以使用errno输出错误原因。

例如,打开一个不存在的文件:

ifstream input("aabbcc.txt", ios::in);
if (!input) {
  cout << "open fail error: " << strerror(errno) << endl;
}

输出结果会打印出文件不存在的错误:

使用形式:

  • using指令(using directive)的使用形式为using namespace std
  • using声明(using declaration)的使用形式为using std::cout

作用差别:

  • using指令的作用是让std内的所有声明在当前文件作用域内都可用,我们可以使用域作用符::直接访问std命名空间内的所有定义。
  • 而using声明只是在当前作用域内声明std命名空间内的一个函数,只有被声明的函数才能在当前作用域内使用。

使用建议:using声明的作用域更小,可以更有效地缩小使用作用域,避免函数冲突问题,更推荐使用。

一、const和成员函数的故事

const的用途有以下几种:

  1. 修饰全局、局部、成员变量
  2. 修饰成员函数

修饰变量的时候const限制了变量在整个程序运行期间都是不能修改的,而修饰成员函数的时候限制函数内不能修改数据成员,这应该是所有C++程序员都烂熟于心的准则。但除了这两条准则以外,const还有一些隐含的准则,是写代码的时候是很容易被忽视的。例如以下代码,相信很多人都无法一眼看出这段代码的问题在哪:

class Hero {
private:
    std::string name;
public:
    Hero(const std::string &name) :name(name) {
    }

    ~Hero() {}

    std::string GetName() {
        return name;
    }
};

void PrintHeroName(const Hero &h) {
    std::cout << h.GetName() << std::endl;
}

其实问题很简单:PrintHeroName函数的形参是一个const类型的Hero对象,但是在函数体内,这个const类型的Hero对象调用了一个非const的成员函数,这是不允许的。这种情况下,编译器会报错:

const_cast.cpp:19:18: error: 'this' argument to member function 'GetName' has type 'const Hero', but function is not marked const
    std::cout << h.GetName() << std::endl;
                 ^
const_cast.cpp:13:17: note: 'GetName' declared here
    std::string GetName() {
                ^
1 error generated.

如果想要在PrintHeroName函数中调用h.GetName方法,只需要把形参里的const去掉就可以了。或者把GetName修改成一个const属性的成员函数。

其实最开始我也没有发现这段代码的问题,作为亲手写下这段代码的始作俑者,我甚至还捉摸了一段时间。但这其实就是一个简单的用法,因为使用得过于习惯,并且自认为非常熟悉,所以就忽视了问题所在。

二、为什么要使用const_cast

const_cast的作用是去掉变量的const或者volatile限定符——这看起来很鸡肋,因为很多人都在想:我既然要去掉const限制,那我直接不使用const修饰不就完事了吗?何必多此一举?其实最开始我也是这么认为的,所以从来没有用过这个关键字。直到今天遇到上面的那个问题之后,才突然明白const_cast的真正用途。

上面的程序,把PrintHeroName形参的const去掉后,程序可以正常编译了。问题是,如果调用它的上层函数,Hero对象已经被const限定了,应该怎么办?例如:

void PrintHeroName(Hero &h) {
    std::cout << h.GetName() << std::endl;
}

void CreateHero(const Hero &h) {
    PrintHeroName(h);
}

可以看到,CreateHero的形参是const的,但PrintHeroName是非const的,无法在CreateHero函数中将h传递到PrintHeroName,因为不能直接将一个const对象转换成非const的。程序编译也会报错:

const_cast.cpp:23:5: error: no matching function for call to 'PrintHeroName'
    PrintHeroName(h);
    ^~~~~~~~~~~~~
const_cast.cpp:18:6: note: candidate function not viable: 1st argument ('const Hero') would lose const qualifier
void PrintHeroName(Hero &h) {
     ^
1 error generated.

这种情况下就要用到const_cast了,这里才是const_cast大展拳脚的地方:因为我们很明确能知道GetName()是不会修改任何成员对象的值的,所以可以在这里通过const_cast去掉Hero的const限定,使得程序可以正常往下调用。即,只要把h通过const_cast包裹起来就可以了:

void CreateHero(const Hero &h) {
    PrintHeroName(const_cast<Hero &>(h));
}

一、前言

昨天在公司做代码扫描,发现很多类似以下的代码都产生了告警,导致扫描不通过:

virtual int func() override {}

不通过的原因是:同时使用virtual和override关键字来修饰成员函数,virtual关键字是多余的,要删掉。

说实话,刚开始看到错误提示的时候有点懵,因为对这个特性并不是很了解(代码也不是我写的),所以一时之间也不知道到底是什么原因,只是贸然按照提示把virtual关键字删掉了(删掉了就好了),回来研究了一阵之后才搞明白。

二、override和final

2.1 用途

override和final是C++11中的新特性,主要用于类继承时对虚函数的控制:

  • override修饰子类成员函数,表明当前成员函数覆盖了父类的成员函数。
  • final修饰父类成员函数,表明当前成员函数不能被覆盖。

其实看到这里我心里有一个疑惑:加了virtual关键字就可以实现覆盖了,为什么要用override呢?C++ Primer对这个问题的解释是:

派生类可能定义了一个和父类名字相同但是形参列表不同的成员函数,对编译器而言这不是非法的,这可能就导致不可预期的错误。可能我们是想覆盖父类的函数,但是因为不小心弄错了,最后编译器也没能帮我们检查出来。

加上override关键字之后,如果子类的函数在父类没有相同的函数名以及形参定义,编译器会报错。这就避免了因为开发人员不小心导致的意外错误。

因此,总结来看,override的作用主要是:

  1. 减少程序员因为大意出错的可能性
  2. 提高代码可读性,读代码的人一看到override就能直观的知道当前函数是覆盖了父类的虚函数

2.2 示例

class Hero {
public:
    virtual void SkillR(Hero &b) {};
};
 
class Soldier : public Hero{
public:
    Soldier() {}
     
    void SkillR(Hero &b) override {}
};

以上定义了一个英雄类和一个继承于它的战士类,战士类继承了父类的R技能SkillR(),它的函数名和形参列表和父类一模一样,加上override之后是没有问题的。但是如果把SkillR的形参去掉,编译时就会报错。

override.cpp:15:19: error: non-virtual member function marked 'override' hides virtual member function
override.cpp:8:18: note: hidden overloaded virtual function 'Hero::SkillR' declared here: different number of parameters (1 vs 0)
    virtual void SkillR(Hero &b) {};
                 ^
1 warning and 1 error generated.

三、override和virtual

回到问题本身,为什么virtual碰到override会失效?

当我使用clion编写上面的IDE也提示virtual是多余的:

原因:

cppreference.com中找到override的定义为:

Specifies that a virtual function overrides another virtual function.

意思是说,override指定函数是一个覆盖了其他类虚函数的虚函数,它本身的定义就是一个虚函数。相当于override=virtual+重写,因此virtual关键字也就多余了。