blueyi's notes

Follow Excellence,Success will chase you!

0%

C/C++基础知识拾遗

C/C++中一些之前的学习中没有涉及到和容易模糊的知识点。
其他专题标签链接:C++拾遗

构造函数中需要初始化列表初始化的成员

在构造函数中需要初始化列表初始化的有如下三种情况

  1. 带有const修饰的类成员 ,如const int a
  2. 引用成员数据,如 int &p; (可以参见这里C/C++拾遗之类成员含引用变量
  3. 带有引用的类变量 (例如类A中含有有引用成员,类B中定义的类型为A的变量)

static 修饰的变量在类外初始化,const修饰的在参数列表初始化

面向对象程序设计的SOLID原则

S.O.L.I.D是面向对象设计和编程(OOD&OOP)中几个重要编码原则(Programming Priciple)的首字母缩写。

缩写 全称 中文
SRP The Single Responsibility Principle 单一责任原则
OCP The Open Closed Principle 开放封闭原则
LSP The Liskov Substitution Principle 里氏替换原则
DIP The Dependency Inversion Principle 依赖倒置原则
ISP The Interface Segregation Principle 接口分离原则

C/C++位运算符

运算符优先级为从上到下递减,<<,>>优先级相同。

操作符 功能 用法
~ 位求反 ~expr
<< 左移 expr1 << expr2
>> 右移 expr1 >> expr2
& 位与 expr1 & expr2
^ 位异或 expr1 ^ expr2
一个竖杠 位或 expr1

其中左移/右移是指expr1中各位向左/右移动expr2位
位异或:相异为真,或者有一个为真,另一个为假即为真(或是有一个为真,即为真)。
关于位运算的几个用法

  1. x = x & (x - 1);会消掉x的二进制中最后一位为1的位,如7&6=0111&0110=0110,即将7最后一位1变成了0
  2. (!(x & (x-1)) && x),返回1则n为2的正整数幂,根据1中的特点以及2的正整数幂的二进制中只有一个1,如1000表示16,&&x是为了排除0
  3. (x & 1),如果返回值为0,则x为偶数,如果返回值为1,则x为奇数。
  4. x << 1相当于乘以2,x >> 1相当于除以2。相应的左多k位就是乘以2^k
  5. x & ((1 << x) - 1) 表示x mod 2^k
  6. x = x | (x + 1);会消掉x的二进制中最后一位为0的位,如10|11=1010|1011=1011,即将10的二进制的最后一位0变成了1

不能声明为虚函数的函数

虚函数是为了实现动态绑定,不能声明为虚函数的有:

  1. 静态成员函数 (静态成员函数不与任何对象绑定,也没有this指针,所有对象及类共用同一份代码,所以不能是虚函数)
  2. 类外的普通函数; (虚函数本身就是为了实现通过父类的指针来动态绑定到子类的虚函数的,没类自然没的说)
  3. 构造函数;(C++语言规定,网上这个讨论很多,也可以理解为构造函数不能被继承)
  4. 友元函数 (友元函数不能被继承)

而对于内联函数(inline修饰)是可以声明为虚函数的,只是说,由于虚函数需要在运行时动态绑定(即根据动态类型需要进行虚函数调用),导致内联函数会被编译器在编译函数忽略,而当成普通函数来处理。因为内联函数是指在编译阶段,编译器使用函数的定义体来迭代函数调用语句,从而减少函数的调用来降低函数运行时间,如果内联函数在编译阶段被展开,那么运行时实际上是不存在内联函数的,虚函数定义成内联函数的话,编译器为了支持动态绑定,也就只能忽略内联函数了,毕竟inline关键字也只是给编译器建议,编译器是可以根据需要进行忽略的

内联函数优缺点

内联函数是指函数在声明时前面加上关键字inline,用来降低程序的运行时间。编译器在编译阶段将使用内联函数的定义体来替代内联函数调用语句,注意这种替代行为发生在编译阶段而非程序运行阶段(所以虚函数声明成的内联会被编译器忽略)。
另外内联函数仅仅是对编译器的内联建议,编译器是否采取你的建议取决于函数是否符合内联的有利条件。如果函数体非常大或者虚函数,那么编译器将忽略函数的内联声明,而将内联函数作为普通函数处理。

优点:

  • 它通过避免函数调用所带来的开销来提高程序的运行速度。因为 函数调用发生时,它节省了变量弹栈、压栈的开销和函数执行完返回原现场的开销。
  • 通过将函数声明为内联,你可以把函数定义放在头文件内,而无需担心编译时遇到函数重复定义的错误

缺点:

  • 因为代码的扩展,内联函数增大了可执行程序的体积。
  • 内联函数中代码过多,会造成很大的内存消耗
  • 不允许有循环或者开关语句,如果有的话,执行函数代码时间比调用开销大

sizeof(‘a’)的大小

直接看下面这段代码:

1
2
3
4
5
6
7
#include<stdio.h>
int main()
{
char c='a';
printf("%d %d\n",sizeof(c),sizeof('a'));
return 0;
}

使用gcc以C语言源文件编译运行结果为

1
1 4

而使用g++以C++语言源文件编译运行结果为

1
1 1

因为:
C99标准规定,’a’叫做整型字符常量(integer character constant),被看成是int型,所以在32位机器上占4字节。
ISO C++标准规定,’a’叫做字符字面量(character literal),被看成是char型,占1字节

需要做为成员函数重载的运算符

  • 只能作为成员函数重载的运算符有:=、()、[]、->、new、delete。
  • 单目运算符最好重载为成员函数。
  • 对于复合的赋值运算符如+=、-=、*=、/=、&=、!=、~=、%=、>>=、<<=建议重载为成员函数。
  • 对于其它运算符,建议重载为友元函数。

C++中不能被重载的运算符

  • 类属关系运算符.
  • 成员指针运算符.*
  • 作用域运算符::
  • sizeof运算符
  • 三目运算符?:
  • tpeid
  • const_cast
  • dynamstic_cast
  • reinterpret_cast
  • static_cast

补码

下述描述中原数值是指绝对值,如-5的原数值为5
首先记住整数二进制的最高位即表示符号位,符号位为0表示正数,符号为1表示负数。
计算机中整数的二进制是以补码的形式存储在内存中的。一个正数的补码和它的原码相同,当然对于正数来说从其补码求原数值直接转化二进制即可。而负数的补码是该数的绝对值的二进制形式按位取反再加1(或者说是该数值的除符号位之外的各位按位取反再加1,符号位为1)。对于负数来说,从其补码求原数值也是除符号位之外按位取反再加1。
例如10的原码和补码(假定是2个字节)都是(符号位为0)

1
0000000000001010

求-10的补码,直接将上述二进制除符号位按位取反再加1(符号位为1):

1
1111111111110110

从-10的补码求其原数值10的二进制,还是除符号位,各位按位取反再加1(符号位为0):

1
0000000000001010

转成10进制就是10,加符号位成-10
例如:求执行int x = 1; int y = ~x;后y的值
首先1在内存中的补码是其原码(还假定是2个字节,4字节也一样):

1
0000000000000001

按位取反后赋给y:

1
1111111111111110

由y的二进制求其原码,由于符号位为1,即负数,所以除符号位按位取反再加1即是y的原数值的二进制

1
0000000000000010

转成10进制即2,加符号位,y为-2

C语言math.h中的abs

C中函数abs申明为 int abs(int num);
num为0或正数时,函数返回num值;
当num为负数且不是最小的负数时,函数返回num的对应绝对值数,即将内存中该二进制位的符号位取反,并把后面数值位取反加一;
当num为最小的负数时(即0x80000000),由于正数里int类型32位表示不了这个数的绝对值,所以依然返回该负数。

C语言文件读写

C语言中的文件操作函数定义在stdio.h头文件中,主要的文件操作函数有:

  • FILE * fopen(const char *filename, const char *mode);以模式mode打开名为filename的文件
  • int fclose(FILE *stream); //关闭一个给定的文件流stream
  • fread()和fwrite() 实现对文件流中整块数据的读/写
  • fgetc()和fputc() 对文件流读/写一个字符
  • fgets()和fputs() 从指定的文件流读/写字符串
  • fscanf()和fprintf() 对指定的文件流格式化读/读
  • ftell() 返回文件流的当前读写位置
  • fseek() 将文件流指针当前的读写位置移到指定的位置
  • rewind() 将文件指针重新指向指定流的开头

C++格式化输出

C++的cout,确切来说是ostream都是可以格式化输出的,格式化输入输出(Input/output manipulators)所用到的函数主要定义在头文件ios,istream,ostream和iomanip中,参数链接:
Input/output manipulators
例如:
1.按输出4位以下的整数num,并且不足4位的位补0,如12,输出为0012

1
std::cout << std::setw(4) << std::setfill('0') << 12 << std::endl;

2.输出double,保留小数位后4位:

1
std::cout << std::setiosflags(std::ios::fixed) << std::setprecision(6) << pi << std::endl;

C++时间相关头文件

C++下面可以统计时间的相关头文件有两个,一个是C风格的ctime,和C++11中新增的chrono,chrono是一个模板类,当然chrono的优点更多,功能也更强大,可以看SOF上的这个比较和解释what is the difference between chrono and ctime
需要注意的是chrono下的函数及类型都定义在头文件std::chrono下。下面来自cppreference上的一个例子给出了它们两个很简单的用法
下面我会一句句加注释来解释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>  //标准输出头文件,没什么好说的
#include <iomanip> //用于格式化输入输出的头文件
#include <chrono> //C++11中新增加的时间操作头文件
#include <ctime> //C-style的时间操作头文件
#include <thread> //C++11中用于多线程的头文件

// the function f() does some time-consuming work
void f()
{
volatile double d = 0;
for(int n=0; n<10000; ++n)
for(int m=0; m<10000; ++m)
d += d*n*m;
}

int main()
{
std::clock_t c_start = std::clock(); // C-style时间函数,返回从当前进程开启到调用该函数时CPU时钟记录的近似时间,必须要除以CLOCKS_PER_SEC来获取以秒为单位的时间
auto t_start = std::chrono::high_resolution_clock::now(); //返回当前的最高精确时钟时间,返回值类型为std::chrono::time_point<std::chrono::high_resolution_clock>
std::thread t1(f);
std::thread t2(f); // f() is called on two threads
t1.join();
t2.join();
std::clock_t c_end = std::clock();
auto t_end = std::chrono::high_resolution_clock::now();

//通过clock()两次返回值的差获得其两次调用期间所经历的时间
//通过模板类std::chrono::duration的count函数来获得指定精度的时间时隔
std::cout << std::fixed << std::setprecision(2) << "CPU time used: "
<< 1000.0 * (c_end-c_start) / CLOCKS_PER_SEC << " ms\n"
<< "Wall clock time passed: "
<< std::chrono::duration<double, std::milli>(t_end-t_start).count()
<< " ms\n";
}

输出是

1
2
CPU time used: 1590.00 ms
Wall clock time passed: 808.23 ms

C++内存布局

参见这里实例分析C++内存布局

C++标准在哪看

SOF上的高票回答给出的非常详细Where do I find the current C or C++ standard documents?

C/C++ GCC编译器标准库源码

可以使用locate来定位所需要的头文件,使用locate之前需要先root运行updatedb来更新locate所需要的数据库文件。如查看string的头文件可能的位置locate string,当然头文件通常都在/usr/include/及其子文件夹下,配合grep筛选查找。

Welcome to my other publishing channels