C++

C++语法及基础

运算符

运算符是一种告诉编译器执行特定的数学或逻辑操作的符号。C 语言内置了丰富的运算符,并提供了以下类型的运算符:

  • 算术运算符
  • 关系运算符
  • 逻辑运算符
  • 位运算符
  • 赋值运算符
  • 杂项运算符

位运算符

位运算符作用于位,并逐位执行操作。&、 | 和 ^ 的真值表如下所示:

p q p & q p | q p ^ q
0 0 0 0 0
0 1 0 1 1
1 1 1 1 0
1 0 0 1 1

假设如果 A = 60,且 B = 13,现在以二进制格式表示,它们如下所示:

A = 0011 1100

B = 0000 1101

-----------------

A&B = 0000 1100

A|B = 0011 1101

A^B = 0011 0001

~A = 1100 0011

下表显示了 C 语言支持的位运算符。假设变量 A 的值为 60,变量 B 的值为 13,则:

运算符 描述 实例
& 按位与操作,按二进制位进行"与"运算。运算规则: 0&0=0; 0&1=0; 1&0=0; 1&1=1; (A & B) 将得到 12,即为 0000 1100
| 按位或运算符,按二进制位进行"或"运算。运算规则: 0|0=0; 0|1=1; 1|0=1; 1|1=1; (A | B) 将得到 61,即为 0011 1101
^ 异或运算符,按二进制位进行"异或"运算。运算规则: 0^0=0; 0^1=1; 1^0=1; 1^1=0; (A ^ B) 将得到 49,即为 0011 0001
~ 取反运算符,按二进制位进行"取反"运算。运算规则: ~1=-2; ~0=1; (~A ) 将得到 -61,即为 1100 0011,一个有符号二进制数的补码形式。
<< 二进制左移运算符。将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。 A << 2 将得到 240,即为 1111 0000
>> 二进制右移运算符。将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。 A >> 2 将得到 15,即为 0000 1111

Mangling

重载,包括 函数重载操作符重载

函数重载

函数重载 也叫 方法重载。是编译器通过把原方法名称与其参数相结合产生一个独特的内部名字来取代原方法名称的技术。

基本上,支持函数重载的语言都需要进行Name Mangling。Mangling的目的就是为了给重载的函数不同的签名,以避免调用时的二义性调用。

Name Mangling 不是一个非常新的技术,在C语言中也有,***在汇编C语言时经常看到的以 下划线“_”开头的函数名,其实就是C编译器将函数名进行了 Name Mangling*** 。

但是在C++中Name-mangling要复杂的多。 因为C++中支持 overloadoverride ,这就导致了C++编译器必须要有完成的Name-mangling把函数名或者变量名进行调整。

在面向对象编程语言出现之前,如果你想要打印不同类型的数据,需要写多个方法 ,象是 PrintInteger(int i)PrintString(string s)PrintFloat(float f) 。也就是说必须通过命名来区别行为和数据类型,因为 OOP语言出现前,任一语言都(像是C)不允许使用相同的名字命名函数, 即使参数类型不同。

但在C++中,像是 Print(int i)、Print(string s) 和 Print(float f),编译器自会准确调用特定的Print方法。当调用 Print(1) 的时候, 编译器可能在内部用源于参数类型的前缀重命名Print方法,这样一来 Print(1) 可能就变成 i_Print (1)

下面是更详细的例子:

C++编译器实际上将下面这些重载函数:

1
2
3
4
void print(int i);
void print(char c);
void print(float f);
void print(char* s);

编译为:

1
2
3
4
_print_int
_print_char
_print_float
_pirnt_string

这样的函数名,来唯一标识每个函数。

注:不同的编译器实现可能不一样,但是都是利用这种机制。所以当连接是调用 print(3) 时,它会去查找 _print_int(3) 这样的函数。下面说个题外话,正是因为这点,重载被认为不是多态,多态是运行时动态绑定(“一种接口多种实现”),如果硬要认为重载是多态,它顶多是编译时“多态”。

C++中的变量,编译也类似,如全局变量可能编译g_xx,类变量编译为c_xx等。连接是也是按照这种机制去查找相应的变量。

方法重载 仅是多态性的一种情形。

名称重整 是一种支持方法重载的机制。更普遍的情况下,多态性是与继承相联系。

Inherit

继承就是一个新类 (称为子类) 从被继承类(称为父类或超类)取得自身的部分定义同时增加一些自己的新的信息。

如果你在相同的类中重载方法, 数据类型必须是不同的。如果你在继承关系下重载方法, 子类与父类的方法可能完全相同,而且名称重整器生成同样的重整名称。

举例来说,假设一个超类定义一个 Print(int i) 方法而一个从它继承的子类也定义了一个 Print(int i) 方法。当你有一个子类的实例时,运用多态性调用 Child.Print(int) ;而当你产生一个父类的实例时运用多态性调用 Parent.Print(int) 。这就是继承多态性:相同的名字和签字但是类却不同。

继承多态性 是通过使用一种与名称重整相关的另外一种机制实现的。编译器把方法放置在一个被称为虚拟方法表(其实是一个方法数组)的地方。每一个方法在VMT中都有一个索引, 如此当 Print(int) 被调用的时候, 编译器将被路由到VMT处找寻Print方法和类的内在索引。这样一来,编译器就可以调用正确的方法实现。由编译器负责管理所有的VMT索引和类偏移量。

简言之,多态性使你能够用非常相似的名字定义许多方法,这里的名字往往都是直观易记的。 OOP编译器自会根据调用者类理解到底该调用哪个方法。

Only one version of an overloaded function can appear within the extern C block. The code in the following example would result in an error.

While you can use name overloading in your SYS/BIOS C++ applications, only one version of the overloaded function can be called from the configuration.

1
2
3
4
extern “C” { // Example causes ERROR
Int addNums(Int x, Int y);
Int addNums(Int x, Int y, Int z); // error, only one version of addNums is allowed
}

Inline Function

内联方法 即内联函数,成员函数,inline functions,是指定义在类体内的函数。

该函数可以在类体内被声明和定义,也可以在类体内声明同时在体外使用 inline 关键字进行定义,如:

1
2
3
4
5
6
7
8
9
10
class angle{
private:
double value;
public:
void SetValue(double);
}

inline void angle::SetValue(double x){ //定义内联函数
value = x;
}

注意:内联函数无法递归。

Constructor and Destructor

构造函数(Constructor Function) 在调用时会为对象开辟储存空间、作初始化 及 其他管理操作。

  • 如果为编写,则系统默认生成
  • 可以接受参数不能有返回值
  • 可以有多个构造函数,因此可以接受名称重载(Name Mangling)。

析构函数(Destructor Function) 仅在释放对象的内存空间时使用,如 程序超出类对象的作用域类指针运行delete运算符 时。

Friend Function

友元函数 是指 在类内部声明,可以 自由访问该类的私有部分 并且 不属于类成员函数

在友元函数声明时定义一个该类的对象,可以通过引用该对象作为参数进行对类的访问。

为了确保数据完整性并遵循数据封装和隐藏的原则,因尽量少用或不用友元。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Student;
class Teacher{
public:
//....
}
protected:
int NoOfStudent;
Student * pList[100];
};

class Student{
public:
friend class Teacher; //友元类声明
//....
}

Preprocessing

定义

预处理是指将源文件的文本作为翻译的第一阶段操作的文本处理步骤。 预处理不会分析源文本,但会为了查找宏调用而将源文本细分为标记。 主要包括了下面三个方面:

  • 预处理指令
  • 预处理运算符
  • 预定义宏,这个有很多了,比如__FILE__、__LINE__和__DATA__等。

常识 - 预处理并不是编译,也不是“预编译” - 预处理并不是每个语言都有 - C/C++预处理仅仅是把源程序划分和整理成一个个的段(phase),并不进行编译。 - 预处理器在UNIX传统中通常缩写为PP,在自动构建脚本中C预处理器被缩写为CPP的宏指代。为了不造成歧义,C++(c-plus-plus) 经常并不是缩写为CPP,而改成CXX

Preprocessing Directives 预处理指令

#include #import #using #progma
#if #ifdef #ifndef #elif
#lese #endif #define #undef
#error #line

预处理运算符号

运算符 操作
字符串化运算符(#) 导致对应的实参括在双引号内
Charizing运算符(#@)
标记粘贴运算符(##)
定义的运算符

# and

字符串化运算符

# 除了是 预处理符号,也是一种 运算符 ,即 字符串化运算符,只能出现在带参的宏的替换文本中,将跟在后面的参数转换成一个字符串常量

1
2
3
4
5
#define PF_INT(i) printf(#i"=%d\n",i)
void main(){
int x=100;
PF_INT(x);
}

预处理后:

1
printf("x""=%d\n",x);

C语言常将相邻的字符串合并处理:

1
printf("x=%d\n",x);
标记粘贴运算符

## 是一种 运算符 ,即 标记粘贴运算符,是将两个 运算对象 连接(拼接)在一起,只能出现在带参宏定义的替换文本中。如:

1
#define NUM(h,t,u)  h##t##u

假设u代表个位,t代表十位,h代表百位,则x=NUM(1,2,3)后,x=123。

注:##也可以用于拼接一些开头一样,尾巴不一样的宏,这样的宏一般用于描述代表特定意义的对象的不同状态等。宏开头固定,根据不同条件则选择拼接不同尾巴,最后拼接的字符串代表一个具体的状态等。

Differences between C & C++

  • 从语法要求来说,C++的语法要求更为严格,编译器对参数变量的检查要求更高,更容易报错;很多在C中可以被顺利编译的语句,在C++中会被严厉拒绝,例如,在C++中,当形参为unsigned char,而实参为const char时会报错。
  • C是面向过程语言,代码复用复杂。C++是面向对象语言。

C++11

定义与声明

变量 只可以被定义(definition)一次,但是可以被多次声明(declaration)。

extern 是跨文件编译(分离式编译,seperate compilation)的 变量声明符,只要没有显式(explicitly)地初始化变量,如extern int num = 100 就可以,任何包含了显式初始化的声明,即成为了定义

关键字

关于用户自定义的标识符,需要注意的是:

  • 不能连续出现两个下划线。
  • 不能以下划线紧连大写字母开头。
  • 定义在函数体外的标识符不能以下划线开头。
image-20210801144743194

顶层const

指针本身是一个对象,指针本身可以指向另一个对象。

“指针本身是不是常量” 和 “指针所指的是不是一个常量” 是两个互相独立的问题。

顶层const(top-level const)表示 指针本身是个常量

底层const(low-level const)表示 指针所指的对象是一个常量

初始化

C++语言定义了初始化的好几种不同 形式,如下:

1
2
3
4
int sold = 0;
int sold = {0};
int sold{0};
int sold(0); //亲测可用

注意初始化并不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义把对象的当前值擦除,并用一个新的值来替代

拷贝初始化(Copy Initialization):使用等号(=)初始化一个变量,编译器把等号右侧的初始值拷贝到新创建的对象上去。

直接初始化(Direct Initialization):不使用等号进行初始化。

列表初始化(List Initialization):无论是为对象初始化或赋新值,都可以使用一组由花括号括起来的初始值。

命名空间

image-20211009225305135

这里说的头文件 不应包含,是只说不建议包含,

常见问题及错误

C++常见的内存错误及解决方法

(1)内存分配未成功,却使用了它。

在使用内存之前先检查指针是否是NULL。如果是用malloc来申请内存,应该用if(p == NULL)或if(p != NULL)进行防错处理。如果是new来申请内存,申请失败会抛出异常,所以应该捕捉异常来进行防错处理。

(2)内存虽然分配成功,但尚未初始化就引用它。

尽管有时候缺省时会自动初始化,但无论什么时候创建对象均要对其进行初始化,即使是赋0值也是不可忽略的。

(3)内存分配成功,但访问越界

对数组for循环时要把握越界,否则可能会导致数组越界。

(4)忘记释放内存,导致内存泄漏

动态内存的申请和释放必须配对,new-delete和malloc-free其使用次数必须相等。

(5)已经释放内存还在使用它

free或delete后 ,没有将指针设为NULL,产生“野指针”。

C++中struct与class的区别是什么?

如果没有多态和虚拟继承,在C++中,struct和class的存取效率完全相同,存取class的数据成员与非虚函数效率和struct完全相同,不管该数据成员是定义在基类还是派生类。

class的数据成员在内存中的布局不一定是数据成员的声明顺序,C++只保证处于同一个access section的数据成员按照声明顺序排列。

C++中,class和struct做类型定义是只有两点区别:

  • 默认继承权限不同,class继承默认是private继承,而struct默认是public继承
  • class还可用于定义模板参数,像typename,但是关键字struct不能同于定义模板参数。

C++保留struct关键字,原因:

  • 保证与C语言的向下兼容性,C++必须提供一个struct。
  • C++中的struct定义必须百分百地保证与C语言中的struct的向下兼容性,把C++中的最基本的对象单元规定为class而不是struct,就是为了避免各种兼容性要求的限制。
  • 对struct定义的扩展使C语言的代码能够更容易的被移植到C++中。

如何将结构体传递给函数?

与类对象一样,结构体变量也可以通过值、引用和常量引用传递给函数。

默认情况下,它们通过值传递,这意味着需要生成整个原始结构的副本并传递给函数。

因为不希望浪费时间来复制整个结构体,所以,除非结构很小,否则一般会通过 引用 将结构体传递给函数。但是,这样意味着函数可以访问原始结构的成员变量,从而可能更改它们。

如果不想让函数更改任何成员变量值,那么可以考虑将结构体变量作为一个 常量引用 传递给函数。

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
35
36
37
38
39
40
41
42
43
44
45
46
//程序1
#include <iostream>
#include <iomanip>
#include <string>
using namespace std;

struct Invltem // Holds data for an inventory item
{
int partNum; // Part number
string description; // Item description
int onHand; // Units on hand
double price; // Unit price
};
// Function prototypes
void getltemData(InvItem &) ; //普通引用,函数可能会对结构体数据造成影响

void showItem(const InvItem &); //常量引用,不会让函数对结构体造成数据变化

int main()
{
InvItem part; // Define an Invltem structure variable.
getItemData(part);
showItem(part);
return 0;
}

void getItemData(InvItem &item)
{
cout << "Enter the part number: ";
cin >> item.partNum;
cout << "Enter the part description: ";
cin.get();
getline (cin, item.description);
cout << "Enter the quantity on hand: ";
cin >> item.onHand;
cout << "Enter the unit price: ";
cin >> item.price;
}
void showItem(const InvItem &item)
{
cout << fixed << showpoint << setprecision(2) << endl;
cout << "Part Number : " << item.partNum << endl;
cout << "Description : " << item.description << endl;
cout << "Units On Hand : " << item.onHand << endl;
cout << "Price : $" << item.price << endl;
}

程序输出结果:

1
2
3
4
5
6
7
8
9
Enter the part number: 800
Enter the part description: Screwdriver
Enter the quantity on hand: 135
Enter the unit price: 1.25

Part Number : 800
Description : Screwdriver
Units On Hand: 135
Price : $1.25

如何从函数返回一个结构体?

也可以从函数返回结构体变量。在这种情况下,函数的返回类型是结构体的名称。可以改写程序 1 以允许 getItemData 函数创建 Invltem 结构体的局部实例,将数据值放入其成员变量中,然后将其传递回 main,而不是将其作为引用变量从 main 接收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
InvItem getItemData()
{
InvItem item;
cout << "Enter the part number:";
cin >> item.partNum;
cout << "Enter the part description: ";
cin.get();
getline(cin, item.description);
cout << "Enter the quantity on hand: ";
cin >> item.onHand;
cout << "Enter the unit price: ";
cin >> item.price;
return item;
}

以下是从 main 中调用它的方法:

1
part = getItemData();

注意: C++ 只允许从函数返回单个值。然而,结构体提供了解决这一限制的方法。即使一个结构体可能有几个成员,它在技术上还是一个单一的对象。通过在结构体中打包多个值,可以从函数返回任意数量的值。

. | -> | :: | :符号区分?

  1. A.B则A为对象或者结构体;

  2. A->B则A为指针,->是成员提取,A->B是提取A中的成员B,A只能是指向类、结构、联合的指针;

  3. :: 是作用域运算符,A::B表示作用域A中的名称B,A可以是名字空间、类、结构;

  4. : 一般用来表示继承;

计算机上正在运行的句柄、线程、进程分别是什么意思?

https://www.cnblogs.com/bluestorm/p/5712238.html

所谓 句柄 实际上是一个数据,是一个Long (整长型)的数据。

句柄是WONDOWS用来标识被应用程序所建立或使用的对象的唯一整数,WINDOWS使用各种各样的句柄标识诸如应用程序实例,窗口,控制,位图,GDI对象等等。WINDOWS句柄有点像C语言中的文件句柄。

从上面的定义中的我们可以看到,句柄是一个 标识符,是拿来标识对象或者项目的,它就像我们的姓名一样,每个人都会有一个,不同的人的姓名不一样,但是,也可能有一个名字和你一样的人。从数据类型上来看它只是一个 16位的无符号整数应用程序几乎总是通过调用一个WINDOWS函数来获得一个句柄,之后其他的WINDOWS函数就可以使用该句柄,以引用相应的对象。

句柄是一种 指向指针的指针。所谓指针是一种内存地址。应用程序启动后,组成这个程序的各对象是驻留在内存的。如果简单地理解,似乎只要获知这个内存的首地址,那么就可以随时用这个地址访问对象。但是非也,Windows是一个以虚拟内存为基础的操作系统。在这种系统环境下,Windows内存管理器经常在内存中来回移动对象,依此来满足各种应用程序的内存需要。对象被移动意味着它的地址变化了。如果地址总是如此变化,该到哪里去找该对象呢?

为了解决这个问题,Windows操作系统为各应用程序腾出一些 内存储地址,用来专门登记各应用对象在内存中的地址变化,而这个地址(存储单元的位置)本身是不变的Windows内存管理器在移动对象在内存中的位置后,把对象新的地址告知这个句柄地址来保存。这样我们只需记住这个句柄地址就可以间接地知道对象具体在内存中的哪个位置。这个地址是在对象装载(Load)时由系统分配给的,当系统卸载时(Unload)又释放给系统。

句柄地址(稳定)→记载着对象在内存中的地址→对象在内存中的地址(不稳定)→实际对象

本质:WINDOWS程序中并不是用物理地址来标识一个内存块,文件,任务或动态装入模块的,相反的,WINDOWS API给这些项目分配确定的句柄,并将句柄返回给应用程序,然后通过句柄来进行操作。

但是必须注意的是程序每次从新启动,系统不能保证分配给这个程序的句柄还是原来的那个句柄,而且绝大多数情况的确不一样的。假如我们把进入电影院看电影看成是一个应用程序的启动运行,那么系统给应用程序分配的句柄总是不一样,这和每次电影院售给我们的门票总是不同的一个座位是一样的道理。

线程 是指程序的一个指令执行序列,WIN32 平台支持多线程程序,允许程序中存在多个线程。 在单 CPU 系统中,系统把 CPU 的时间片按照调度算法分配给各个线程,因此各线程实际上是分时执行的,在多 CPU 的 Windows NT 系统中, 同一个程序的不同线程可以被分配到不同的 CPU 上去执行。由于一个程序的各线程是在相同的地址空间运行的,因此设及到了如何共享内存, 如何通信等问题,这样便需要处理各线程之间的同步问题,这是多线程编程中的一个难点。

线程,也被称为轻量进程(lightweight processes)。计算机科学术语,指运行中的程序的调度单位。

线程是进程中的实体,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程不拥有系统资源,只有运行必须的一些数据结构;它与父进程的其它线程共享该进程所拥有的全部资源。线程可以创建和撤消线程,从而实现程序的并发执行。一般,线程具有 就绪阻塞运行 三种基本状态。

在多中央处理器的系统里,不同线程可以同时在不同的中央处理器上运行,甚至当它们属于同一个进程时也是如此。大多数支持多处理器的操作系统都提供编程接口来让进程可以控制自己的线程与各处理器之间的关联度(affinity)。

进程 是程序在一个数据集合上运行的过程(注:一个程序有可能同时属于多个进程),它是操作系统进行资源分配和调度的一个独立单位,进程可以简单的分为 系统进程 (包括一般Windows程序和服务进程)和 用户进程

预编译

#Error 预编译中断及错误提示

Encountered with text

错误信息会在预编译期遇到错误的时候停止并给出,也就是错误信息的上一条执行代码出现了错误。

而上一条代码是 #if !defined( __BYTE_ADDRESSING__ ) && defined ( __ADSPSHARC__ ) ,需要综合给出的错误提示 “Only Byte addressing mode is supported” 。

image-20210520154525815

Symbol could not be resolved

此类情况都是 “未定义” 或 “定义重复”。按住 左ctrl 并点击该定义可快速查看,是否定义或重复定义。

重复定义

以下错误的出现则是因为出现了两处重复定义:

image-20210520154603077
image-20210521093104044

未定义

NULL通常在 <stddef.h> (标准定义)头文件中给出,通常这些文件也被 <stdlib.h><stdio.h> 头文件引用。

image-20210520154631949

也可以进行自定义:

1
2
3
#ifndef NULL
#define NULL ((void *)0)
#endif

以下代码源自 <stddef.h> :

1
2
3
4
5
6
7
#ifndef NULL
#if defined (__cplusplus)
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif

参考Eclipse CDT “Symbol NULL could not be resolved”

image-20210520232442191

Error: li1050 - MULTIPLY DEFINED SYMBOL

错误编号

Error li1050

解决方法

将错误提示中所涉及到的变量(Tx_Count)或函数(initPLL_SDRAM()Init_TWI())分离至单独的一个 .c 文件中,在名称对应的头文件中,仅做函数及变量的声明,详细内容不要写在头文件中。

头文件中不能声明全局变量。

image-20210521163422688

实践

将错误提示中的变量或函数逐一移动到新的同名 .c 文件后,错误提示也在变少:

减少一个image-20210524100548198

减少两个image-20210524100816631

问题解决image-20210524101126445

解决方法的灵感来源于EZ论坛上的发言(以下回答其实也没细看):

image-20210524101743988