Qida's Blog

纸上得来终觉浅,绝知此事要躬行。

A type alias is a different name by which a type can be identified. In C++, any valid type can be aliased so that it can be referred to with a different identifier.

在 C++ 中,有两种创建类型别名的语法:

  1. 从 C 语言继承而来,使用 typedef 关键字:

    1
    typedef existing_type new_type_name ;
  2. 由 C++ 语言引入,使用 using 关键字:

    1
    using new_type_name = existing_type ;

existing_type 可以是任何类型,无论是基本类型还是复合类型:

例子一:

typedef using
typedef char C; using C = char;
typedef unsigned int WORD; using WORD = unsigned int;
typedef char * pChar; using pChar = char *;
typedef char field [50]; using field = char [50];

例子二,下面两种定义结构体类型的方式是等价的:

1
2
3
4
5
6
7
8
9
struct product {
int weight;
double price;
};

typedef struct {
int weight;
double price;
} product;

new_type_name 作为该类型的别名,用法如下:

1
2
3
4
C mychar, anotherchar, *ptc1;
WORD myword;
pChar ptc2;
field name;

一旦定义了这些别名,就可以像其它有效类型一样,在任何声明中使用。尤其常见于与结构体搭配使用。

参考

https://www.cplusplus.com/doc/tutorial/other_data_types/

声明语法

1
2
3
4
5
6
7
struct type_name {
member_type1 member_name1;
member_type2 member_name2;
member_type3 member_name3;
.
.
} object_names;

定义用法

结构体对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// declare three objects (variables) of this type (product): apple, banana, and melon.
struct product {
int weight;
double price;
} apple, banana, melon;

// struct requires either a type_name or at least one name in object_names, but not necessarily both.
struct {
int weight;
double price;
} apple, banana, melon;

// declare three objects (variables) of this type (product): apple, banana, and melon.
product apple, banana, melon;

结构体数组

1
2
// because structures are types, they can also be used as the type of arrays.
product banana[3];

结构体指针

1
product * p = &apple;

创建结构体指针之后,可以使用以下运算符访问其成员变量:

Operator Expression What is evaluated Equivalent
dot operator (.) a.b Member b of object a
arrow operator (->)
(dereference operator)
a->b Member b of object pointed to by a (*a).b

例子:

1
2
3
4
5
apple.weight;  // 3
// The arrow operator (->) is a dereference operator that is used exclusively with pointers to objects that have members. This operator serves to access the member of an object directly from its address.
p->weight; // 3
// equivalent to:
(*p).weight; // 3

例子 2:

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
#include <iostream>
using namespace std;

struct product {
int weight;
double price;
};

void test() {
product apple;
apple.weight = 3;
apple.price = 4;
product * p_apple = &apple;

cout << "weight: " << (*p_apple).weight << endl; // weight: 3
cout << "weight: " << p_apple->weight << endl; // weight: 3

cout << "address of apple: " << p_apple << endl; // address of apple: 0x7ffee256b5b0
cout << "address of weight: " << &p_apple->weight << endl; // address of weight: 0x7ffee256b5b0
cout << "address of price: " << &p_apple->price << endl; // address of price: 0x7ffee256b5b8
cout << "size of weight: " << sizeof(p_apple->weight) << endl; // size of weight: 4
cout << "size of price: " << sizeof(p_apple->price) << endl; // size of price: 8
}

int main() {
test();
return 0;
}

结构体作为函数参数

支持三种方式的传参:

  • 传值
  • 传引用
  • 传址
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
#include <iostream>
using namespace std;

struct product {
int weight;
double price;
} lemon;

// 传值
void show(product prd) {
cout << "weight: " << prd.weight << " price: " << prd.price << endl;
}

// 传引用
void show2(product &prd) {
cout << "weight: " << prd.weight << " price: " << prd.price << endl;
}

// 传址
void show3(product * prd) {
cout << "weight: " << prd->weight << " price: " << prd->price << endl;
}

void test() {
lemon.weight = 1;
lemon.price = 2;
show(lemon); // weight: 1 price: 2
show2(lemon); // weight: 1 price: 2
show3(&lemon); // weight: 1 price: 2
}

int main() {
test();
return 0;
}

参考

http://www.cplusplus.com/doc/tutorial/structures/

重点:区分下述四种形式:

1
2
3
4
5
6
7
// 字符数组(Character sequences)
char str[] = {'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '\0'};
char str[] = "hello world";
// 字符串指针
char *str = "hello world";
// 字符串类(string)
string str = "hello world";

字符数组

字符数组(Character sequences):http://www.cplusplus.com/doc/tutorial/ntcs/

字符串实际上是使用 null 字符 \0 终止的一维字符数组,如下:

C 字符串

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 字符串实际上是使用 null 字符 \0 终止的一维字符数组
char str[] = {'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '\0'};
// 依据数组初始化规则,简化如下:
char str2[] = "hello world";
// conversion from string literal to 'char *' is deprecated
char *str3 = "hello world";

// str: hello world size: 12
cout << "str: " << str << " size: " << sizeof(str) << endl;
// str2: hello world size: 12
cout << "str2: " << str2 << " size: " << sizeof(str2) << endl;
// 'sizeof (str3)' will return the size of the pointer, not the array itself
// str3: hello world size: 8
cout << "str3: " << str3 << " size: " << sizeof(str3) << endl;

字符串指针

用字符数组和字符串指针都可实现字符串的存储和运算,但是两者是有区别的:

  • 字符数组是一个数组,每个元素的值都可以改变。
  • 而字符串指针指向的是一个常量字符串,它被存放在程序的静态数据区,一旦定义就不能改变

这是最重要的区别。下面的代码在运行期间将会出错:

1
2
str2[1] = 'a';      // hallo world
*(str3 + 1) = 'a'; // 运行时出错。因为不能改变字符串常量的值

string 字符串类

string 头文件提供了 string 类,参考:http://www.cplusplus.com/reference/string/

cstring 操纵器

cstring 头文件提供了大量的函数,用来操纵 C strings and arrays,参考:http://www.cplusplus.com/reference/cstring/

例如:

  • Copying:
  • Concatenation:
  • Comparison:
  • Searching:
    • strchr Locate first occurrence of character in string
    • strrchr Locate last occurrence of character in string
  • Other:

参考

https://www.cplusplus.com/doc/tutorial/ntcs/

https://www.runoob.com/cplusplus/cpp-strings.html

C++输出char型变量与字符串的地址

https://stackoverflow.com/questions/1524356/c-deprecated-conversion-from-string-constant-to-char

一维数组

一维数组声明:type arrayName[ arraySize ];

1
2
// 声明数组
int array1[5];

一维数组初始化:

1
2
3
4
// 初始化数组
float array1[5] = {1, 2, 3, 4, 5};
// 初始化数组(省略数组大小声明,默认大小为初始化时元素的个数)
float array2[] = {1, 2, 3, 4, 5};

一维数组访问:

1
2
3
4
5
6
// 通过索引逐个访问数组元素,并赋值
array1[0]; // 1
array1[1]; // 2
array1[2]; // 3
array1[3]; // 4
array1[4]; // 5

多维数组

多维数组声明:type name[ size1 ][ size2 ]...[ sizeN ];

1
2
// 声明一个三维 5 . 10 . 4 整型数组
int array1[5][10][4];

二维数组初始化:

1
2
3
4
5
6
7
8
9
// 多维数组可以通过在括号内为每行指定值来进行初始化。下面是一个带有 3 行 4 列的数组。
int a[3][4] = {
{0, 1, 2, 3} , /* 初始化索引号为 0 的行 */
{4, 5, 6, 7} , /* 初始化索引号为 1 的行 */
{8, 9, 10, 11} /* 初始化索引号为 2 的行 */
};

// 内部嵌套的括号是可选的,下面的初始化与上面是等同的:
int a[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};

二维数组访问:

1
2
3
4
5
6
7
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
for (int j = 0; j < sizeof(a[0]) / sizeof(a[0][0]); j++)
{
cout << a[i][j] << endl;
}
}

二维数组大小及长度获取:

1
2
3
4
5
6
7
8
// 数组总大小:48
sizeof(a)
// 数组第一维大小:16
sizeof(a[0])
// 数组第一维长度:3
sizeof(a) / sizeof(a[0])
// 数组第二维长度:4
sizeof(a[0]) / sizeof(a[0][0])

数组名

数组名是指向数组中第一个元素的常指针(常量指针),因此 arrayName 等价于 &arrayName[0],验证如下:

1
2
3
int arrayName[] = {0, 1, 2, 3, 4};
cout << arrayName << endl; // 0x7ffeec364620
cout << &arrayName[0] << endl; // 0x7ffeec364620

由于数组是常指针,因此自身无法执行算术运算:

1
2
3
4
5
6
7
8
// 表达式必须是可修改的左值
// invalid operands to binary expression
arrayName += 1;
arrayName -= 1;
arrayName++;
arrayName--;
++arrayName;
--arrayName;

数组构成

  • 存储一个固定大小相同数据类型元素的顺序集合。
  • 连续的内存地址组成。

验证如下:

1
2
3
for (int i = 0; i < 5; i++) {
cout << &arrayName[i] << endl;
}

输出如下:

1
2
3
4
5
0x7ffeec364620
0x7ffeec364624
0x7ffeec364628
0x7ffeec36462c
0x7ffeec364630

分析上述输出结果,由于 1 个内存单元的大小是 8 bits,即一个字节。而一个 int 类型的变量占用 4 个字节,因此上述 16 进制表示的内存地址的递增步长为 4。

获取数组大小

通过 sizeof 运算符,确认该数组大小为 20 字节:

1
sizeof(arrayName) // 20

获取数组长度

sizeof 运算符用于获取变量的存储大小(即所占内存字节数),由于数组中每个元素的类型都是一样的、所占字节数亦然,因此可以计算如下:

1
sizeof(arrayName) / sizeof(arrayName[0]);  // 5

参考:C 语言数组传入函数获取数组长度的方法

遍历数组元素

使用数组索引访问数组元素:

1
2
3
for (int i = 0; i < 5; i++) {
cout << arrayName[i] << endl; // 1 2 3 4 5
}

指针数组 VS 数组指针

指针数据 VS 数组指针

指针数组

指针数组,表示“存储指针的数组”,即定义一个数组,其每个元素都是指针:

1
int *p1[10];

数组指针

数组指针,表示“指向数组的指针”,即定义一个指针,指向数组:

1
int *p2 = new int[10];

常用于定义函数的形式参数

1
void func(int *p);

优点:数组作为常指针不能执行算术运算,但数组指针可以。下面通过递增指针,以顺序访问数组元素:

1
2
3
4
5
6
7
8
int arrayName[5] = {1, 2, 3, 4, 5};  // &arrayName = 0x7ffee7376620
int *p = arrayName;

for (int i = 0; i < 5; i++)
{
cout << p << endl; // 0x7ffee7376620 0x7ffee7376624 0x7ffee7376628 0x7ffee737662c 0x7ffee7376630
cout << *p++ << endl; // 1 2 3 4 5
}

传递数组给函数

三种方式

有三种方式传递数组给函数。

方式一:形式参数是一个数组指针(指向数组的指针)。

1
2
3
4
void func(int *p)
{
cout << sizeof(p) << endl; // 返回指针长度 8 bytes
}

方式二:形式参数是一个已定义大小的数组。

1
2
3
4
5
void func(int p[5])
{
// sizeof on array function parameter will return size of 'int *' instead of 'int [5]'
cout << sizeof(p) << endl; // 返回指针长度 8 bytes
}

方式三:形式参数是一个未定义大小的数组。

1
2
3
4
5
void func(int p[])
{
// sizeof on array function parameter will return size of 'int *' instead of 'int []'
cout << sizeof(p) << endl; // 返回指针长度 8 bytes
}

注意点

无论使用何种方式,函数内都无法获取数组长度,需要使用单独变量将数组长度作为参数传入。

1
2
3
4
5
6
7
void func(int p[], int length)
{
for (int i = 0; i < 5; i++)
{
cout << p[i] << endl; // 1 2 3 4 5
}
}

参考

http://www.cplusplus.com/doc/tutorial/arrays/

https://www.runoob.com/cplusplus/cpp-arrays.html

引用

引用是一个别名,也就是说,它是某个已存在变量的另一个名字。修改引用等同于修改被引用变量自身。

一个变量可以有多个引用。

声明引用

不存在空引用。声明引用的同时,必须初始化,否则报编译错误如下:

1
int &ref;  // declaration of reference variable 'ref' requires an initializer

引用一旦初始化,就不能再指向另一个变量。修改引用等同于修改被引用变量本身。

指针

指针是一个变量,其值为另一个变量的地址,即,内存位置的直接地址。

声明指针

一元运算符 * 用于声明一个指针变量:

1
2
3
4
5
bool   *bp;    // 声明一个布尔型的空指针
char *ch; // 声明一个字符型的空指针
int *ip; // 声明一个整型的空指针
double *dp; // 声明声明一个 double 型的空指针
float *fp; // 声明一个浮点型的空指针
  • 可以声明空指针。
  • 除了常指针,其它指针可以在任何时间被初始化。
  • 所有指针的值的实际数据类型,不管是布尔型、字符型、整型、浮点型,还是其它的数据类型,都是一样的,其值都是一个代表内存地址十六进制数
  • 不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同,因此执行递增或递减时的步长不同

获取指针大小

操作系统 指针变量的存储大小
32 bits 4 Bytes
64 bits 8 Bytes

本机为 64 位操作系统,验证如下:

1
2
3
4
5
sizeof(bool*)  // 8
sizeof(char*) // 8
sizeof(int*) // 8
sizeof(float*) // 8
sizeof(double*) // 8

指针的算数运算

可以对指针进行四种算术运算:++--+-。运算后,指针保存新的地址。

1
2
3
4
5
6
7
8
9
10
11
int a = 0;
int b = 1;
int * p = &a;

p = &b;
p += 1;
p -= 1;
p++;
p--;
++p;
--p;

重点考点

1
2
3
4
5
6
7
8
// 常指针
int* const p = &a;

// 指向常量的指针
const int * p;

// 指向常量的常指针
const int * const p = &a;

常指针

顾名思义,指针本身是个常量,不能修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
int a = 0;
int b = 1;
int* const p = &a;

// 表达式必须是可修改的左值
// cannot assign to variable 'p' with const-qualified type 'int *const'
p = &b;
p += 1;
p -= 1;
p++;
p--;
++p;
--p;

常见的常指针,例如:

  • 数组名

指向常量的指针

顾名思义,指针指向的是常量,不能修改其值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int a = 0;
int b = 1;
const int * p = &a;

// 指针本身可以重新赋值
p = &b;

// 但不能修改指针指向的常量的值,否则报编译错误:
// 表达式必须是可修改的左值
// read-only variable is not assignable
*p += 1;
*p -= 1;
(*p)++;
(*p)--;
++(*p);
--(*p);

指向常量的常指针

结合了上述两种特性。

常见指针

字符串指针

1
2
// 字符串指针(指向一个常量字符串,它被存放在程序的静态数据区,一旦定义就不能改变)
char * str = "hello world";

数组指针

1
2
3
4
5
6
7
8
int arrayName[5] = {1, 2, 3, 4, 5};  // &arrayName = 0x7ffee7376620
int * p = arrayName; // // 数组指针(Pointers to array,即指向数组中第一个元素的地址)

for (int i = 0; i < 5; i++)
{
cout << p << endl; // 0x7ffee7376620 0x7ffee7376624 0x7ffee7376628 0x7ffee737662c 0x7ffee7376630
cout << *p++ << endl; // 1 2 3 4 5
}

结构体指针

1
2
3
4
5
6
7
product apple;
// 结构体指针(Pointers to struct)
product * p = &apple;
// The arrow operator (->) is a dereference operator that is used exclusively with pointers to objects that have members. This operator serves to access the member of an object directly from its address.
p->weight;
// equivalent to:
(*p).weight;

类指针

1
2
3
4
5
6
7
8
9
10
Rectangle rect(3, 4);
// 类指针(Pointers to classes),主要用于多态性
Shape * p = &rect;

// member y of object x
rect.area(); // 12
// member y of object pointed to by x
p->area(); // 12
// equivalent to:
(*p).area(); // 12

引用与指针对比

常见问题,下列代码的区别?

1
2
3
4
*p
&p
*&p
&*p

要解决这个问题,首先需要了解这两个运算符的区别:

在赋值运算符左侧 在赋值运算符右侧
* 表示声明指针 表示取值运算符
& 表示声明引用 表示取址运算符

代码示例如下:

1
2
3
4
5
6
7
int a = 10;
int& b = a;
int* p = &a;

cout << "a = " << a << ", &a = " << &a << ", *&a = " << *&a << endl;
cout << "b = " << b << ", &b = " << &b << ", *&b = " << *&b << endl;
cout << "p = " << p << ", &p = " << &p << ", *p = " << *p << ", &*p = " << &*p << ", *&p = " << *&p << endl

输出结果如下:

1
2
3
a = 10, &a = 0x7ffee483763c, *&a = 10
b = 10, &b = 0x7ffee483763c, *&b = 10
p = 0x7ffee483763c, &p = 0x7ffee4837628, *p = 10, &*p = 0x7ffee483763c, *&p = 0x7ffee483763c

总结如下:

指针变量 结果 描述
p 0x7ffee483763c 返回指针变量 p 保存的地址
*p 10 返回指针变量 p 保存的地址的实际值
&p 0x7ffee4837628 返回指针变量 p 自身的地址
&*p 0x7ffee483763c 返回指针变量 p 保存的地址的实际值的地址
*&p 0x7ffee483763c 返回指针变量 p 自身的地址的实际值

引用与指针的区别,如下图:

引用与指针对比

参考

http://www.cplusplus.com/doc/tutorial/pointers/

Difference between pointer and reference in C ?

C++ 中的参数传递方式:传值、传地址、传引用总结

32/64 位系统支持多大内存?

函数定义

函数定义形式如下:

  • 函数头

    • 返回类型

    • 函数名称

    • 形式参数

      • 无参数函数

      • 有参数函数

        • 参数默认值(必须从右到左赋默认值)

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          // 函数默认值,必须从右到左
          int max2(int num1, int num2 = 100)
          {
          return num1 > num2 ? num1 : num2;
          }

          // 否则报编译错误:missing default argument on parameter 'num2'
          int max3(int num1 = 100, int num2)
          {
          return num1 > num2 ? num1 : num2;
          }
  • 函数体

函数声明

函数必须先声明后使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
using namespace std;

// 函数必须先声明后使用
int max1(int num1, int num2);

int main()
{
cout << "result is " << max1(1, 10) << endl; // 否则报错:未定义标识符 use of undeclared identifier 'max1'
return 0;
}

int max1(int num1, int num2)
{
return num1 > num2 ? num1 : num2;
}

函数参数

函数的实际参数有三种传递方式:

调用类型 调用类型 例子 描述
传值 传值调用 void swap(int x, y) 把实际参数的实际值复制一份给形式参数。修改函数内的形式参数对实际参数没有影响
传址 指针调用 void swap(int * x, int * y) 把实际参数的地址赋值给形式参数。在函数内,该指针用于访问实际参数的地址。这意味着,修改形式参数会影响实际参数
传引用 引用调用 void swap(int &x, &y) 把实际参数的引用赋值给形式参数。在函数内,该引用作为实际参数的别名。这意味着,修改形式参数会影响实际参数

传址与传引用的使用区别,如下:

指针与引用的使用区别

函数调用

函数的调用方式:

  • 嵌套调用
  • 递归调用(直接递归, 间接递归)

参考

http://www.cplusplus.com/doc/tutorial/functions/

一、预处理器

指令 描述
#include 包含一个源代码文件(扩展名为 .h 的头文件
#define
#undef
定义宏
取消已定义的宏
#if
#else
#elif
#endif
条件编译

参考:

二、头文件

https://www.runoob.com/cprogramming/c-header-files.html

https://www.runoob.com/cprogramming/c-standard-library.html

三、基础语法

1、标识符(identifier)

是用来标识变量、函数、类、模块,或任何其他用户自定义项目的名称。

一个标识符只能以:

  • 字母 A-Za-z 或下划线 _ 开始;
  • 后跟零个或多个字母、下划线和数字(0-9)。

2、保留字(关键字)

3、注释

单行注释(行注释)://

多行注释(块注释):/* ... */

4、变量与常量

C++ 变量类型

C++ 数据类型

C++ 修饰符类型

C++ 变量作用域

4.1、基本数据类型

C 语言的基本数据类型:

类型 关键字 存储大小 备注
布尔型 bool sizeof(bool) = 1 字节
字符型 char sizeof(char) = 1 字节
整型 int sizeof(int) = 4 字节
浮点型 float sizeof(float) = 4 字节 C++ 中,小数默认为浮点型。
双浮点型 double sizeof(double) = 8 字节

基本数据类型

C 语言各种数据类型的内存映像(32 位平台):

基本数据类型

参考:

【计算机如何编码大数和小数-哔哩哔哩】 https://b23.tv/8rEeTf0

4.2、修饰符

C++ 允许在 charintdouble 数据类型前放置修饰符。修饰符用于改变基本类型的含义,所以它更能满足各种情境的需求。

修饰符 int double char
signed Y Y
unsigned Y Y
long Y Y
short Y

4.3、作用域

在 C++ 中:

  • 作用域可分为:
    • 全局作用域
    • 局部作用域
    • 语句作用域
  • 作用域优先级:范围越小,优先级越高
  • 如果希望在局部作用域中使用同名的全局变量,可以在该变量前使用:作用域运算符 ::
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
using namespace std;

// 全局变量
int x = 10;

int main()
{
// 局部变量
int x = 100;
cout << x << endl; // 100
cout << ::x << endl; // 10
}

4.4、常量定义

在 C++ 中,有两种简单的定义 C++ 常量的方式:

  • 使用 #define 预处理器进行宏定义
  • 使用 const 关键字。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;

// 定义常量(使用预处理器)
#define LENGTH 10
#define WIDTH 5
#define NEWLINE '\n'

int main()
{
// 定义常量(使用 const 关键字)
const int LENGTH = 10;
const int WIDTH = 5;
const char NEWLINE = '\n';

return 0;
}

5、运算符

5.1、位运算符

5.2、算术运算符

运算符 描述 实例
+ 把两个操作数相加 A + B 将得到 30
- 从第一个操作数中减去第二个操作数 A - B 将得到 -10
* 把两个操作数相乘 A * B 将得到 200
/ 分子除以分母 B / A 将得到 2
% 取模运算符,整除后的余数 B % A 将得到 0
++ 自增运算符,整数值增加 1 A++ 将得到 11
-- 自减运算符,整数值减少 1 A– 将得到 9

5.3、赋值运算符

运算符 描述 实例
= 简单的赋值运算符,把右边操作数的值赋给左边操作数 C = A + B 将把 A + B 的值赋给 C
+= 加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数 C += A 相当于 C = C + A
-= 减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数 C -= A 相当于 C = C - A
*= 乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数 C *= A 相当于 C = C * A
/= 除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数 C /= A 相当于 C = C / A
%= 求模且赋值运算符,求两个操作数的模赋值给左边操作数 C %= A 相当于 C = C % A
<<= 左移且赋值运算符 C <<= 2 等同于 C = C << 2
>>= 右移且赋值运算符 C >>= 2 等同于 C = C >> 2
&= 按位与且赋值运算符 C &= 2 等同于 C = C & 2
^= 按位异或且赋值运算符 C ^= 2 等同于 C = C ^ 2
|= 按位或且赋值运算符 C |= 2 等同于 C = C | 2

5.4、关系运算符

运算符 描述 实例
== 检查两个操作数的值是否相等,如果相等则条件为真。 (A == B) 不为真。
!= 检查两个操作数的值是否相等,如果不相等则条件为真。 (A != B) 为真。
> 检查左操作数的值是否大于右操作数的值,如果是则条件为真。 (A > B) 不为真。
< 检查左操作数的值是否小于右操作数的值,如果是则条件为真。 (A < B) 为真。
>= 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。 (A >= B) 不为真。
<= 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。 (A <= B) 为真。

5.5、逻辑运算符

运算符 描述 实例
&& 称为逻辑与运算符。如果两个操作数都 true,则条件为 true。 (A && B) 为 false。
|| 称为逻辑或运算符。如果两个操作数中有任意一个 true,则条件为 true。 (A || B) 为 true。
! 称为逻辑非运算符。用来逆转操作数的逻辑状态,如果条件为 true 则逻辑非运算符将使其为 false。 !(A && B) 为 true。

5.6、其它运算符

运算符 描述
:: 作用域运算符 Scope operator,用于引用全局变量 ::code、引用某个命名空间的函数或变量 namespace::code 等等。
& 取地址运算符 Address-of operator (&) 返回变量的地址。例如 &a 将给出变量的实际内存地址。
* 间接寻址运算符 Dereference operator (*) 指向一个变量。例如,*var 返回操作数所指定地址的变量的值。
.(点)和 ->(箭头) 成员运算符用于引用结构体共用体的成员。
sizeof sizeof 运算符返回变量的存储大小。例如,sizeof(int) 返回 4 个字节。
Cast 强制转换运算符把一种数据类型转换为另一种数据类型。例如,int(2.2000) 将返回 2。

6、控制语句

C 语言有九种控制语句。 可分成以下三类:

6.1、选择语句

  • ifelse 语句
  • switch 语句

6.2、循环语句

  • while 语句
  • do while 语句
  • for 语句

6.3、跳转语句

  • break 语句
  • continue 语句
  • goto语句(此语句尽量少用,因为这不利结构化程序设计,滥用它会使程序流程无规律、可读性差)

参考

https://www.cplusplus.com/doc/tutorial/

https://www.cplusplus.com/doc/tutorial/variables/

https://www.cplusplus.com/doc/tutorial/namespaces/

C 语言入门教程 - 阮一峰

环境准备

打开 VScode,进入 Extensions,二选一搜索并安装以下扩展:

方式一:官方插件 C/C++

Using Clang in Visual Studio Code 教程指引如何使用官方插件 C/C++ 进行构建与调试。

按教程配置 .vscode 文件夹的三个文件:

  • tasks.json (编译器构建设置)
  • launch.json (调试器设置)
  • c_cpp_properties.json (编译器路径和 IntelliSense 设置)

⇧⌘B 编译源文件:

1
/usr/bin/clang++ -std=c++17 -stdlib=libc++ -g /Users/wuqd/Documents/workspace/cpp/HelloWorld.cpp -o /Users/wuqd/Documents/workspace/cpp/HelloWorld.out

编译成功后:

  • 可以在「终端」输入 ./HelloWorld.out 以直接运行编译文件;

  • 还可以按 F5 调试。

方式二:三方插件 Code Runner

安装第三方插件:Code Runner

settings.json 新增配置如下,将编译文件的后缀名改为 .out,以便 .gitignore:

1
2
3
"code-runner.executorMap": {
"cpp": "cd $dir && g++ $fileName -o $fileNameWithoutExt.out && $dir$fileNameWithoutExt.out"
}

⌃⌥N 编译并运行:

1
cd "/Users/wuqd/Documents/workspace/cpp/" && g++ HelloWorld.cpp -o HelloWorld.out && "/Users/wuqd/Documents/workspace/cpp/"HelloWorld.out

常见问题

DIFFERENCE BETWEEN g++ & gcc

GCC 代表 GNU Compiler Collections,主要用于编译 C 和 C++ 语言。它还可以用于编译 Objective C 和 Objective C++。编译源代码文件时需要的最重要的选项是源程序的名称,其余每个参数都是可选的,如警告、调试、链接库、目标文件等。

g++ 命令是 GNU C++ 编译器调用的命令,用于源代码的预处理、编译、汇编和链接以生成可执行文件。

g++ gcc
g++ is used to compile C++ program. gcc is used to compile C program.
g++ can compile any .c or .cpp files but they will be treated as C++ files only. gcc can compile any .c or .cpp files but they will be treated as C and C++ respectively.
Command to compile C++ program through g++ is g++ fileName.cpp -o binary command to compile C program through gcc is gcc fileName.c -o binary
Using g++ to link the object files, files automatically links in the std C++ libraries. gcc does not do this.
g++ compiles with more predefined macros. gcc compiles C++ files with more number of predefined macros. Some of them are #define GXX_WEAK 1, #define __cplusplus 1, #define __DEPRECATED 1, etc

DIFFERENCE BETWEEN gcc & make

gcc是编译器 而make不是 make是依赖于Makefile来编译多个源文件的工具 在Makefile里同样是用gcc(或者别的编译器)来编译程序.

gcc是编译一个文件,make是编译多个源文件的工程文件的工具。

make是一个命令工具,是一个解释makefile中指令的命令工具。

make就是一个gcc/g++的调度器,通过读入一个文件(默认文件名为Makefile或者makefile),执行一组以gcc/g++为主的shell命令序列。输入文件主要用来记录文件之间的依赖关系和命令执行顺序。

gcc是编译工具;

make是定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译;

也就是说make是调用gcc的。

cannot edit in read-only editor

GBK to UTF-8 乱码问题

https://www.cnblogs.com/kingsonfu/p/11010086.html

参考

Using Clang in Visual Studio Code

http://www.cplusplus.com/doc/tutorial/introduction/

C++ 在线编译器

C++ Insights

C++ Compiler Explorer

VSCode 其它插件推荐:

  • C/C++ Clang Command Adapter
  • C++ Intellisense

日志门面:

日志门面——SLF4J

优点:

  • 通过依赖配置,在编译期静态绑定真正的日志框架库。

    What has changed in SLF4J version 2.0.0?

    More visibly, slf4j-api now relies on the ServiceLoader mechanism to find its logging backend. SLF4J 1.7.x and earlier versions relied on the static binder mechanism which is no loger honored by slf4j-api version 2.0.x. More specifically, when initializing the LoggerFactory class will no longer search for the StaticLoggerBinder class on the class path.

    Instead of “bindings” now org.slf4j.LoggerFactory searches for “providers”. These ship for example with slf4j-nop-2.0.x.jar, slf4j-simple-2.0.x.jar or slf4j-jdk14-2.0.x.jar.

  • 不需要使用 logger.isDebugEnabled() 来解决日志因为字符拼接产生的性能问题。SLF4J 的方式是使用 {} 作为字符串替换符。

  • 提供了很多桥接方案,以便灵活替换日志库。

缺点:

  • 旧版不支持 Lambda 表达式(slf4j-api-2.0.0-alpha 支持但还未 GA)。但日志框架 Log4j 2.4 及以上版本支持 Java 8 Lambda,参考 12,例如:

    1
    logger.debug("This {} and {} with {} ", () -> this, () -> that, () -> compute());

SLF4J bindings

http://www.slf4j.org/manual.html#swapping

concrete bindings

Logback

https://logback.qos.ch/

推荐使用 SLF4J bound to logback-classic 的经典组合:

1
2
3
4
5
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>

传递依赖如下:

1
2
3
ch.qos.logback:logback-classic:jar:1.2.3:compile
+- ch.qos.logback:logback-core:jar:1.2.3:compile
\- org.slf4j:slf4j-api:jar:1.7.25:compile

Log4j 2.x

https://logging.apache.org/log4j/2.x/

需引入适配层依赖:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j-impl -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.16.0</version>
</dependency>

传递依赖如下:

1
2
3
4
org.apache.logging.log4j:log4j-slf4j-impl:jar:2.16.0:compile
+- org.slf4j:slf4j-api:jar:1.7.25:compile
+- org.apache.logging.log4j:log4j-api:jar:2.16.0:compile
\- org.apache.logging.log4j:log4j-core:jar:2.16.0:runtime

参考:

Log4j 1.x

https://logging.apache.org/log4j/1.2/

On August 5, 2015 the Logging Services Project Management Committee announced that Log4j 1.x had reached end of life.

需引入适配层依赖:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-log4j12 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.32</version>
</dependency>

传递依赖如下:

1
2
3
org.slf4j:slf4j-log4j12:jar:1.7.32:compile
+- org.slf4j:slf4j-api:jar:1.7.32:compile
\- log4j:log4j:jar:1.2.17:compile

java.util.logging (JUL)

需引入适配层依赖:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-jdk14 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.7.32</version>
</dependency>

传递依赖如下:

1
2
org.slf4j:slf4j-jdk14:jar:1.7.32:compile
\- org.slf4j:slf4j-api:jar:1.7.32:compile

日志框架——Logback

Configuration

https://logback.qos.ch/manual/configuration.html

Configuration file syntax

Configuring Appenders

Logback 配置文件样例:

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
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径-->
<springProperty scope="context" name="LOG_PATH" source="log.path" defaultValue="/data/logs"/>
<springProperty scope="context" name="ACTIVE_PROFILE" source="spring.profiles.active" defaultValue="dev" />
<springProperty scope="context" name="LOG_PATTERN" source="log.pattern" defaultValue="[%d{yyyy-MM-dd HH:mm:ss.SSS}] %5p [%t] %logger{50}:%L [%X{traceId}] --> %m%n" />
<springProperty scope="context" name="MONGO_URI" source="spring.data.mongodb.uri" />

<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>

<!-- 每天归档日志文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/project-${ACTIVE_PROFILE}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--日志文件输出的文件名-->
<fileNamePattern>${LOG_PATH}/project-${ACTIVE_PROFILE}-%i.log.%d{yyyyMMdd}</fileNamePattern>
<!--日志文件保留天数-->
<maxHistory>90</maxHistory>
<maxFileSize>100MB</maxFileSize>
<totalSizeCap>20GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>

<!-- 自定义 MongoDB Appender -->
<appender name="MONGO" class="com.test.MongoDBAppender"></appender>

<logger name="org.springframework" level="WARN"/>

<root level="INFO">
<appender-ref ref="FILE" />
<appender-ref ref="STDOUT" />
<appender-ref ref="MONGO" />
</root>

</configuration>

Appenders

有三种 Appenders:

  1. Logback 官方提供的 Appenders。
  2. 第三方提供的 Appenders(logstash、…)。
  3. 自定义 Appenders。

官方 Appenders

Logback 官方提供的 Appenders 如下:http://logback.qos.ch/manual/appenders.html

Appender

其中,常用的两个 Appender 继承结构如下:

  • ch.qos.logback.core.ConsoleAppender
  • ch.qos.logback.core.rolling.RollingFileAppender

OutputStreamAppender

其中,ch.qos.logback.core.rolling.RollingFileAppender 可选的策略类如下:

Policy

第三方 Appenders

https://github.com/logstash/logstash-logback-encoder

Provides logback encoders, layouts, and appenders to log in JSON and other formats supported by Jackson.

自定义 Appenders

通过自定义 Appenders,可以将日志输出到任意位置,如 MongoDB、Kafka 等服务。

下面演示一个简单的同步 Appender,如需异步 Appender,参考这里

首先,新建 Appender,继承自抽象类:

UnsynchronizedAppenderBase

ILoggingEvent

代码如下:

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
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.UnsynchronizedAppenderBase;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoClientDbFactory;

public class MongoDBAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {

private SimpleMongoClientDbFactory mongoDbFactory;
private MongoTemplate mongoTemplate;
private final ReentrantLock lock = new ReentrantLock(false);

@Override
public void start() {
String uri = this.getContext().getProperty("MONGO_URI");
mongoDbFactory = new SimpleMongoClientDbFactory(uri);
mongoTemplate = new MongoTemplate(mongoDbFactory);
super.start();
}

@Override
protected void append(ILoggingEvent eventObject) {
lock.lock();
try {
PayLog log = new PayLog();
log.setTraceId(eventObject.getMDCPropertyMap().getOrDefault(MDCUtils.LOG_TRACE_ID, null));
log.setOper(eventObject.getMDCPropertyMap().getOrDefault(MDCUtils.OPER, null));
log.setLevel(eventObject.getLevel().toString());
log.setLogger(eventObject.getLoggerName());
log.setThread(eventObject.getThreadName());
log.setMessage(eventObject.getFormattedMessage());
log.setTimestamp(eventObject.getTimeStamp());
mongoTemplate.insert(log, "payLog");
} finally {
lock.unlock();
}
}

}

最后,验证 MongoDB 数据。

Encoders & Layouts

https://logback.qos.ch/manual/encoders.html

https://logback.qos.ch/manual/layouts.html

Logback 提供的 ch.qos.logback.core.encoder.Encoderch.qos.logback.core.Layout 的默认实现如下:

Layout

一般有几种使用组合方式:

自定义 Pattern 格式

基于自定义 Pattern 格式,直接使用 ch.qos.logback.classic.encoder.PatternLayoutEncoder 即可:

1
2
3
4
5
6
7
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
...
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>

还可以输出扩展信息,例如 %X{traceId}

预定义格式

基于预定义格式,需要使用 ch.qos.logback.core.encoder.LayoutWrappingEncoder,并搭配相应 Layout 实现。例如输出成 HTML 格式:

1
2
3
4
5
6
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
...
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="ch.qos.logback.classic.html.HTMLLayout" />
</encoder>
</appender>

Mapped Diagnostic Contexts (MDC)

https://logback.qos.ch/manual/mdc.html

To uniquely stamp each request, the user puts contextual information into the MDC, the abbreviation of Mapped Diagnostic Context.

The MDC class contains only static methods. It lets the developer place information in a diagnostic context that can be subsequently retrieved by certain logback components. The MDC manages contextual information on a per thread basis. Typically, while starting to service a new client request, the developer will insert pertinent contextual information, such as the client id, client’s IP address, request parameters etc. into the MDC. Logback components, if appropriately configured, will automatically include this information in each log entry.

Please note that MDC as implemented by logback-classic assumes that values are placed into the MDC with moderate frequency. Also note that a child thread does not automatically inherit a copy of the mapped diagnostic context of its parent.

Logback leverages SLF4J API:

MDC And Managed Threads

https://logback.qos.ch/manual/mdc.html#managedThreads

MDCInsertingServletFilter

https://logback.qos.ch/manual/mdc.html#mis

例子

一、创建 MDCUtils

1
2
3
4
5
6
7
8
9
10
11
12
13
import lombok.experimental.UtilityClass;
import org.slf4j.MDC;

@UtilityClass
public class MDCUtils {

public static final String TRACE_ID = "traceId";

public MDC.MDCCloseable addTraceId(String traceId) {
return MDC.putCloseable(TRACE_ID, traceId);
}

}

二、在合适的位置设置上下文:

  • 在 http 接口的拦截器
  • 在定时任务执行之前

代码如下:

1
2
3
try (MDC.MDCCloseable mdc = MDCUtils.addTraceId(...)) {
// TODO
}

TraceIdInterceptor

三、选择合适的 Appender 并加以配置,参考 Logback 配置文件样例。

参考

https://github.com/logstash/logstash-logback-encoder

细说 Java 主流日志工具库

SpringBoot + MDC 实现全链路调用日志跟踪

Java 中打印日志的正确姿势

实战总结 | 系统日志规范及最佳实践 | 阿里技术

https://github.com/FasterXML/jackson

https://github.com/FasterXML/jackson-docs

Core modules

Core modules are the foundation on which extensions (modules) build upon. There are 3 such modules currently (as of Jackson 2.x):

  • Streaming (docs) (“jackson-core”) defines low-level streaming API, and includes JSON-specific implementations
  • Annotations (docs) (“jackson-annotations”) contains standard Jackson annotations
  • Databind (docs) (“jackson-databind”) implements data-binding (and object serialization) support on streaming package; it depends both on streaming and annotations packages

Annotations

https://github.com/FasterXML/jackson-annotations

这个页面列出了所有 Jackson 2.0 通用注解,按功能分组。

常用注解如下:

  • @JsonProperty
  • @JsonIgnore
  • @JsonIgnoreProperties
  • @JsonInclude
  • @JsonFormat
  • @JsonSerialize
  • @JsonDeserialize

Databind

For all data-binding, we need a com.fasterxml.jackson.databind.ObjectMapper instance:

1
ObjectMapper mapper = new ObjectMapper(); // create once, reuse

参考:

ObjectMapper,别再像个二货一样一直 new 了!

Configuration

https://github.com/FasterXML/jackson-databind/wiki/#reference-manual

POJO

The most common usage is to take piece of JSON, and construct a Plain Old Java Object (“POJO”) out of it.

Serialization:

1
2
3
4
5
MyValue value = mapper.readValue(new File("data.json"), MyValue.class);
// or:
MyValue value = mapper.readValue(new URL("http://some.com/api/entry.json"), MyValue.class);
// or:
MyValue value = mapper.readValue("{\"name\":\"Bob\", \"age\":13}", MyValue.class);

Deserialization:

1
2
3
4
5
mapper.writeValue(new File("result.json"), myResultObject);
// or:
byte[] jsonBytes = mapper.writeValueAsBytes(myResultObject);
// or:
String jsonString = mapper.writeValueAsString(myResultObject);

Generic Collections

You can also handle JDK Lists, Maps.

Serialization:

1
2
Map<String, Integer> scoreByName = mapper.readValue(jsonSource, Map.class);
List<String> names = mapper.readValue(jsonSource, List.class);

Deserialization:

1
2
// and can obviously write out as well
mapper.writeValue(new File("names.json"), names);

Generic Type

如果需要将 JSON 字符串反序列化为泛型,有两种方式:

方式一:TypeReference

方式一:使用 com.fasterxml.jackson.core.type.TypeReference<T>

This generic abstract class is used for obtaining full generics type information by sub-classing; it must be converted to ResolvedType implementation (implemented by JavaType from “databind” bundle) to be used. Class is based on ideas from http://gafter.blogspot.com/2006/12/super-type-tokens.html, Additional idea (from a suggestion made in comments of the article) is to require bogus implementation of Comparable (any such generic interface would do, as long as it forces a method with generic type to be implemented). to ensure that a Type argument is indeed given.

Usage is by sub-classing: here is one way to instantiate reference to generic type List<Integer>:

1
TypeReference ref = new TypeReference<List<Integer>>() { };

which can be passed to methods that accept TypeReference, or resolved using TypeFactory to obtain ResolvedType.

代码如下:

1
2
3
4
5
TypeReference<RespDTO<XxxRespDTO>> typeRef = new TypeReference<RespDTO<XxxRespDTO>>() {};
RespDTO<XxxRespDTO> resp = mapper.readValue(JSON_A, typeRef);

TypeReference<List<XxxRespDTO>> typeRef1 = new TypeReference<List<XxxRespDTO>>() {};
List<XxxRespDTO> resp2 = mapper.readValue(JSON_B, typeRef1);

方式二:JavaType

方式二:使用 com.fasterxml.jackson.databind.JavaType

Base class for type token classes used both to contain information and as keys for deserializers.

Instances can (only) be constructed by com.fasterxml.jackson.databind.type.TypeFactory.

JavaType 的继承结构如下图:

com.fasterxml.jackson.databind.JavaType

代码如下:

1
2
3
4
5
JavaType valueType = mapper.getTypeFactory().constructParametricType(RespDTO.class, XxxRespDTO.class);
RespDTO<XxxRespDTO> resp = mapper.readValue(JSON_A, valueType);

JavaType valueType2 = mapper.getTypeFactory().constructParametricType(List.class, XxxRespDTO.class);
List<XxxRespDTO> resp2 = mapper.readValue(JSON_B, valueType2);

JavaType 的调试结果如下图,其值为 JavaType 的子类 CollectionType

com.fasterxml.jackson.databind.type.CollectionType

Tree Model (JsonNode)

Tree Model can be more convenient than data-binding, especially in cases where structure is highly dynamic, or does not map nicely to Java classes.

com.fasterxml.jackson.databind.JsonNode 表示一个 JSON 树节点,可以通过 ObjectMapper#readTree 方法反序列化出来,也可以通过 JsonNode 的子类 API 自定义构建:

JsonNode

构建代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
JsonNode jsonNode =
new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of(
"hello",
new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(
new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of("key", new TextNode("value0"))),
new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of("key", new TextNode("value1"))),
new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of("key2", new TextNode("value2")))
)),
"test",
new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of(
"key3", new TextNode("value3")))));

// {"hello":[{"key":"value0"},{"key":"value1"},{"key2":"value2"}],"test":{"key3":"value3"}}
log.info(jsonNode.toString());

JsonNode 构建完成后,可以灵活的读取其值,例如:

1
2
3
4
// [value0, value1]
log.info(jsonNode.get("hello").findValuesAsText("key").toString());
// value3
log.info(jsonNode.get("test").get("key3").asText());

也可以修改其值:

1
((ObjectNode) jsonNode).put("key", "value");

使用场景

一、对接口响应的 JSON 原文进行验签:

1
2
3
4
5
6
7
8
9
10
11
{
"response": {
"head": {
...
},
"body": {
...
}
},
"signature": "..."
}
1
2
3
4
JsonNode jsonNode = objectMapper.readTree(responseJson);
String response = jsonNode.get("response").toString();
String signature = jsonNode.get("signature").toString();
return RsaUtil.verify(response, publicKey, signature);

二、<Compare Two JSON Objects with Jackson>

using the JsonNode.equals method. The equals() method performs a full (deep) comparison.

例子

本例中,我们需要获取以下两个方法的泛型返回值中的实际类型参数 XxxRespDTOClass 类型,以用于 JSON 转换:

1
2
3
4
5
6
7
8
9
10
11
12
public interface ApiService {

/**
* 接口一
*/
RespDTO<XxxRespDTO> get(XxxReqDTO reqDTO);

/**
* 接口二
*/
RespDTO<List<XxxRespDTO>> list(XxxReqDTO reqDTO);
}

定义一个方法,用于转换 JSON:

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
private Object getObject(Method method, String json) {
Object result;

// 获取该方法的泛型返回值,然后获取其第一个实际类型参数
ParameterizedType returnType = (ParameterizedType) method.getGenericReturnType();
Type type = returnType.getActualTypeArguments()[0];

// 处理接口一的情况
if (type instanceof Class) {
Class<?> clazz = (Class<?>) type;
result = JsonUtils.fromJson(json, clazz); // ObjectMapper#readValue(String, Class<T>)
} else if (type instanceof ParameterizedType) {
ParameterizedType nestedReturnType = (ParameterizedType) type;
// 处理接口二的情况
if (nestedReturnType.getRawType() == List.class) {
Class<?> clazz = (Class<?>) nestedReturnType.getActualTypeArguments()[0];
result = JsonUtils.fromJsonToList(decryptedRespData, clazz);
} else {
throw new IllegalStateException("未实现的 JSON 解析!");
}
} else {
throw new IllegalStateException("未实现的 JSON 解析!");
}
return result;
}

这种用法常常出现在框架之中。下面来看下调试效果:

接口一

下图展示了变量 returnType 为参数化类型 ParameterizedType,其实际类型参数 typeClass 类型,值为 XxxRespDTO

JsonNode

接口二

下图展示了变量 returnType 的实际类型参数 type 与接口一为 Class 类型不同,接口二为 ParameterizedType 参数化类型,值为 List<XxxRespDTO>

常见报错

Unrecognized field, not marked as ignorable

该错误的意思是说,不能够识别的字段没有标示为可忽略。出现该问题的原因就是 JSON 中包含了目标 Java 对象没有的属性。

解决方案:

  1. 保证传入的 JSON 串不包含目标对象的没有的属性。

  2. On deserialization, @JsonIgnoreProperties(ignoreUnknown=true) ignores properties that don’t have getter/setters

  3. Deserialization Features 全局配置:

    1
    2
    // 配置该 `objectMapper` 在反序列化时,忽略目标对象没有的属性。
    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)

缺少默认构造方法

问题:

1
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.***.RespBody` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)

解决方案:

  • POJO 加上 @NoArgsConstructor

Using Java inner classes for Jackson serialization

https://dev.to/pavel_polivka/using-java-inner-classes-for-jackson-serialization-4ef8

Google Gson

https://github.com/google/gson

1
2
3
4
5
6
7
8
9
10
11
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("time", LocalDateTime.now().toString());
jsonObject.addProperty("ip", ip);
jsonObject.addProperty("rspMillis", rspMillis);
jsonObject.addProperty("rspCode", rspCode);
jsonObject.addProperty("method", method);
jsonObject.addProperty("path", uriPath);
// Deprecated
// new JsonParser().parse(jsonTrace);
jsonObject.add("body", JsonParser.parseString(jsonTrace));
requestJsonLogger.info(jsonObject.toString());

Deserialization to Generic Type: com.google.gson.reflect.TypeToken

1
2
RespDTO<XxxRespDTO> data = new Gson().fromJson(json, new TypeToken<RespDTO<XxxRespDTO>>() {}.getType());
List<XxxRespDTO> data = new Gson().fromJson(json, new TypeToken<ArrayList<XxxRespDTO>>() {}.getType());

参考

https://www.baeldung.com/category/json/jackson/

https://github.com/qidawu/java-api-test/tree/master/src/main/java/json