zl程序教程

您现在的位置是:首页 >  后端

当前栏目

【C++】编程、基础知识

2023-09-27 14:20:37 时间


为什么尽量不要使用using namespace std?

增加了名称冲突的可能性。在一些大的项目中很容易造成和其他的名字冲突,同时容易引起错误,小项目你可能感觉不出来,但这种习惯其实不好,而且也就失去了名字空间本身的意义了

程序在执行int main(int argc, char *argv[])时的内存结构,你了解吗?

  • 参数的含义是程序在命令行下运行的时候,需要输入argc 个参数,每个参数是以char 类型输入的,依次存在数组里面,数组是 argv[],所有的参数为指针类型
  • char * 指向的内存中,数组的中元素的个数为 argc 个,第一个参数为程序的名称。

main函数的参数(int argc,char *argv[])

main函数的返回值有什么值得考究之处吗?

程序运行过程入口点main函数,main()函数返回值类型必须是int,这样返回值才能传递给程序激活者(如操作系统)表示程序正常退出。

main(int args, char **argv) 参数的传递。参数的处理,一般会调用getopt()函数处理,但实践中,这仅仅是一部分,不会经常用到的技能点。

防卫式声明

  • 防卫式声明的作用是:防止由于同一个头文件被包含多次,而导致了重复定义。
  • #ifndef 依赖于宏定义名,当宏已经定义时,#endif之前的代码就会被忽略,但是这里需要注意宏命名重名的问题;
  • #pragma once 只能保证同一个文件不会被编译多次,但是当两个不同的文件内容相同时,仍然会出错。而且这是微软提供的编译器命令,当代码需要跨平台时,需要使用宏定义方式
//1. 宏定义
#ifndef __FILENAME__
#define __FILENAME__
//...
#endif

//2. 编译器指令
#pragma once

const限定符

  • 修饰内置类型
  • 修饰自定义对象
  • const与数组
  • const与引用
  • const与指针(顶层const与底层const)
  • 修饰函数参数
  • 修饰函数返回值
  • 修饰成员函数

1. const修饰内置类型

在定义const变量的时候,必须进行初始化。编译器在编译过程中,会把用到该变量的地方全部替换成相应的值。
默认情况下,const仅在当前文件中有效。若希望多个文件使用同一个const变量,可以使用extern来修改,在任何声明和定义处使用(当然只需要定义一次)。

const int buff_size = 32;
buff_size = 64; // 错误,编译不通过

2. const修饰自定义对象

  • 常对象中的成员变量不可以被修改,且只能调用常成员函数
  • C++ 还提供了一个关键字mutable(参考【C++深陷】之“mutable”),它可以允许我们修改常量对象中的成员变量
const MyClass obj; // 常对象

3. const与数组

  • 必须初始化;不可以执行arr[0] = 1类似的代码。
  • 参考【C++深陷】之“数组”,我们了解编译器通常把数组处理为指向首元素的响应类型的指针,这里也是,类似于const int *类型
//必须初始化;不可以执行arr[0] = 1类似的代码(指向可改,值不可改)
const int arr[] = {2, 3};

4. const与引用

4.1 定义方法

可以把引用绑定到const对象上,我们称之为对常量的引用。

const int ci = 422;
const int &r_ci = ci; // 常量引用
r_ci = 500;  // 错误

4.2 常量引用的初始化

C++的引用必须与其所引用的对象的类型严格一致。

但是在初始化常量引用的时候,可以用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可

//例如非常量对象、字面值、表达式。
int i = 42;
const int &r1 = i;  // 正确,常量引用绑定在一个非const变量上
const int &r2 = 42;  // 正确,常量引用绑定在字面值上
const int &r3 = r1 * 2;  // 正确,常量引用绑定在表达式结果上

当一个常量引用被绑定到另外一种数据类型上的时候,编译器执行了如下的过程:

  1. 生成另一个临时量,用目标对象的值初始化临时量;
  2. 将常量引用绑定到上述的临时量上。

看如下代码解释:

double d = 3.14;
const int &ri = d;

// 会被编译器解释成如下代码
double d = 3.14;
const int temp = d;  // 执行double -> int的隐式转换
const int &ri = temp;  // 将常量引用ri绑定到临时量上

若变量类型和常量引用一样不需要执行转换,则没有生成临时量这一步。

double d = 3.14;
const double &rd = d;  // 不需要生成临时量

C++规定,将左值引用绑定到临时量是非法的;但是却可以将常量引用绑定在临时量上。因此,我们可以将函数的返回值声明成常量引用:

const int& get_a() {
    int a = 3;
    return a;
}

调用get_a将得到数值3。这一点也在【C++深陷】之“引用”中初始化引用的例外情况1提及:可以用临时量初始化常量引用。

这里其实可以用C++11的右值引用来解释。我们可以写成:(初始化右值引用必须是右值)

double d = 3.14;
const int temp = d;  // 执行double -> int的隐式转换
const int &&ri = d; //合法(temp临时对象作为右值)

double d = 3.14;
const double &&rd = d;
// 编译不通过
// 初始化右值引用的必须是一个右值

5. const与指针

5.1 常量指针

和对常量引用一样,我们可以定义一个指向常量的指针:

const double pi = 3.14;
const double *ptr_pi = π

// 错误调用,不可以通过ptr_pi修改pi
*ptr_pi = 3.1415926;
  1. 指针的类型必须与其所指向的类型严格一致。
  2. 但是允许另一个指向常量的指针指向一个非常量对象。这种用法和常量引用类似,但是没有临时量的概念加入基本数据类型必须是严格匹配的
double pi = 3.14;
const double *ptr_pi = π
//这种用法,和常量引用类似,但是没有临时量的概念加入,基本数据类型必须是严格匹配的。

5.2 指针常量

因为指针是C++中的对象,引用仅仅是别名,因此不可以将引用设置为const,但是可以将一个指针声明为const。

实现的效果就是,该指针一旦初始化完成之后,就不能更改指针内容(不能修改地址),即不允许将该指针指向别的对象。

int buff_size = 512;
// pti将始终指向buff_size
int *const pti = &buff_size;

// 允许通过指针常量修改原对象的值
// 因为原对象也是非常量
*pti = 1024;

const double pi = 3.14;
// pt_pi是一个常量的指针常量
const double *const pt_pi = π

5.3 顶层const与底层const

  • 使用术语顶层const,表示指针本身是个常量;
  • 使用术语底层const,表示指向的对象是个常量。
图片名称

更一般的,任意对象的const,都是顶层const

int i = 0;
// 顶层const,修饰普通对象
// 不允许改变ci的值
const int ci = 5;
// 其实可以写成 int const ci = 5;

// 顶层const,修饰指针对象
// 不允许改变cp_i的值
int *const cp_i = &i;

// 底层const,修饰指针指向的对象
// 不允许通过p_ci改变ci的值,即*p_ci = 10
const int *p_ci = &ci;

// 左侧的是底层const,右侧的是顶层const
// 既不允许改变cp_ci的值,也不能使用*cp_ci的改变ci的值
const int *const cp_ci = &ci;

// 底层const,引用都是底层const
// 不允许通过r_ci改变ci的值
const int &r_ci = ci;

注意:只有两处有底层const,一个是指针,一个是引用。

当执行对象的拷贝操作时,常量是顶层const还是底层const区别很大。
顶层const不受什么影响,对拷贝源不做过多要求,因为拷贝只关心拷贝源的值。

// ci是顶层const的常量
// 可以赋值给变量i
i = ci;

// cp_ci有顶层const
// 可以赋值给没有顶层const的p_ci
p_ci = cp_ci;

可以把非底层const的变量赋值给一个底层const

int a = 20;
int *p_a = &a;
// 非底层const赋值给底层const
const int *p_ca = p_a;

但是不可以使用底层const赋值给非底层const

int a = 20;
const int *p_ca = &a;
// 编译出错
// 底层const赋值给非底层const
int *p_a = p_ca;

6. const修饰函数参数

利用传递给函数的实参对函数形参初始化。形参的初始化方式和变量的初始化方式是一样的,因此调用函数传递const实参的规则,和上述的顶层const和底层const的规则一模一样

因为C++支持函数的重载,又因为顶层const的要求很松,若如下声明了两个函数:

void func(const int i);
void func(int i);  // 错误

当我们调用的时候区分不开,因此上述声明重复定义了func,是不允许的,这一点在【C++深陷】之“函数重载”也提及,在【C++深陷】之“函数匹配”中详细介绍了上述示例错误的原因。

C++标准建议,当我们不需要修改某个函数参数的值的时候,尽量将其声明为常量引用,即:

void func(const int &i);

7. const修饰函数返回值

函数返回一个值的方式和初始化一个变量或者形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。

  • 通常情况下,返回基本内置类型,不需要添加const,因为函数返回的是一个右值。
  • 返回指针和引用的时候,如果不添加const,我们可能会写出如下代码:
string &shorter_string(string &s1, string &s2) {
    return s1.size() <= s2.size() ? s1 : s2;
}

shorter_string(s1, s2) = "new string"; //虽然正确,但是很迷惑,通常添加const。
const string &shorter_string(const string &s1, const string &s2) {
    return s1.size() <= s2.size() ? s1 : s2;
}

在第4节我们也提及了返回常量引用的函数,可以返回函数体内的临时量(函数内的局部变量):

// 调用get_a会得到数值3
const int& get_a() {
    int a = 3;
    return a;
}

8. const修饰成员函数

可以如以下方式定义一个const成员函数(常量成员函数):

class MyClass {
private:
    int i = 0;
public:
    int func() const; //常函数
};
// 既包含顶层const,又包含底层const
const MyClass *const this

默认情况下,this的类型是指向类对象的指针常量:

// 这是一个顶层const,this指针的值是不允许被修改的
// 即只能指向本类的一个实例自己
MyClass *const this

紧随参数列表之后的const关键字,其作用是修改隐式指针常量this的类型。

// 既包含顶层const,又包含底层const
const MyClass *const this

关于函数的调用,有以下规则:

  • 若my_class不是常量,且类只有func的const版本,则会调用func的const版本;
  • 若my_class不是常量,同时声明了func的非const版本和const版本,则会调用func的非const版本。
class MyClass {
public:
    int func() const { return 1; }
};

int main() {
    // 不是常量
    MyClass mc;
    // 输出1
    std::cout << mc.func() << std::endl;
}
class MyClass {
public:
    int func() { return 0; }
    int func() const { return 1; }
};

int main() {
    // 不是常量
    MyClass mc;
    // 调用普通版本,输出0
    std::cout << mc.func() << std::endl;
    // 是常量
    const MyClass c_mc;
    // 输出1
    std::cout << c_mc.func() << std::endl;
}

9. 总结

  1. C++中的const的是将对象和函数声明为只读状态的一个关键字。
  2. const可以与基本内置类型使用,此时定义的变量初始化之后就不会改变,编译器会将用到该变量的地方都替换成变量的值。
  3. const可以修饰自定义对象,此时只能调用const成员函数。(常对象只能调用常函数)
  4. const可以与引用使用,表明这是一个对常量的引用,通常称作常量引用。
  5. const可以与指针使用,要区分指向指针常量和常量指针,以及顶层const和底层const的定义。
  6. const可以修饰函数的参数,通常传递常量引用时使用。
  7. const可以修饰函数的返回值,通常在返回指针和引用时使用。
  8. const可以修饰成员函数,修改隐式指针常量this的类型,使常量对象可以调用成员函数。

类型别名(typedef)

//可以使用typedef关键字
typedef unsigned long idx;
//也可以使用别名声明(alias declaration) 技术,这是C++11新标准规定的方法:
using idx = unsigned long;

复合类型的类型别名

typedef double wages;  // 用一个double存储工资 wages
using wages = double; // 同上

class MassachusettsInstituteOfTechnology;
typedef MassachusettsInstituteOfTechnology MIT;
using MIT = MassachusettsInstituteOfTechnology;

1 数组

// 可以表示二维网格的位置
typedef unsigned int ipos[2];
using ipos = unsigned int[2];

能看到typedef的定义方式,理解起来有些不同,但是using声明使用=赋值运算符,看起来直观一些。
理解typedef,我们首先考虑去掉typedef:

unsigned int ipos[2];

这时声明了一个数组变量,名字叫做ipos。这时我们加上typedef,告诉编译器这句话不是声明变量,而是声明类型别名。

// 告诉编译器
// 我声明的不是变量,是类型别名
typedef unsigned int ipos[2];

此时编译器知道,ipos表示的就是包含2个元素类型为unsigned int的数组。

ipos pos = {3, 4};
cout << pos[0] << ", " << pos[1];  // 输出3,4

也可以在声明类型别名的时候,不指定数组的长度:

// 可以表示二维网格的位置
// 也可以是三维、四维...
typedef unsigned int ipos[];
using ipos = unsigned int[];

但是在使用ipos时,必须初始化,告诉编译器数组的长度,这是数组的要求:

ipos pos = {2, 3, 4};  // 此时是三维网格的坐标

很明显使用类型别名更方便阅读

ipos pos = {3, 4};
ipos *p_ipos = &pos;

// unsigned int ipos[2] = {3, 4};
// unsigned int (*p_ipos)[2] = &ipos;

2 引用

typedef MyClass &rmc;
// using rmc = MyClass &

MyClass my_class;
rmc mc = my_class;

此时注意,我们似乎可以定义引用的引用了。

rmc &ref_mc = mc;

请读者注意,某些编译器上述代码是可以编译通过的。C++标准虽然规定了不可以定义引用的引用,但是编译器有自己的实现。而且上述代码,是通过一种间接的方式定义了引用的引用,从字面角度看,依旧是某种类型的引用,这个类型是程序员自己声明的类型,所以也不算是引用的引用。

3 指针

typedef char *pstring;
// using pstring = char *;

pstring ps = "hello world";

通常使用类型别名声明函数指针(前方高能):
typedef string name;
typedef bool (*pCompare)(const name &, const name &);
// using pCompare = bool (*)(const name &, const name &);

bool CompareLength(const name &s1, const name &s2) {
    return s1.size() < s2.size();
}

bool CompareAlpha(const name &s1, const name &s2) {
    return s1[0] < s2[0];
}

int main() {
    const string s1 = "Matt", s2 = "Jimmy";
    vector<pCompare> compares = {CompareLength, CompareAlpha};
    bool flag = false;
    for (const pCompare &pcom : compares) {
        flag = flag || pcom(s1, s2);
    }
    cout << (flag ? "true" : "false") << endl;

    return 0;
}

首先,将string取别名name,表示这个数据类型用来表示名字。
然后声明了一个函数指针pCompare类型,表示那些比较两个const name &,并返回bool类型的函数。
接下来定义的CompareLength和CompareAlpha就是这样的函数。
在main函数中,我们定义了一个类型为vector的compares容器,容纳所有比较操作。
遍历容器中的所有操作,将结果逻辑或||在flag上,输出flag。

这里有一个地方,在范围for循环中:

const pCompare &pcom : compares

仔细推敲一下pcom的类型,它是一个函数指针的常量引用。
若我们不使用typedef,该如何定义呢?

bool (* const &rCom)(const name &, const name &) = &CompareAlpha;

理解上述声明,要使用机器思维:从内向外,从右向左。

从标识符rCom开始。

  • ①rCom向右是右括号,向左;看到&,rCom是引用类型。什么样的引用?
  • ②向左看到const,常量引用;向左看到*,指针的常量引用。什么样的指针?
  • ③看到左括号,向外;看到函数相关的定义,函数指针的常量引用。什么样的函数?
  • ④接下来观察一下函数的定义,就知道是指向什么样的函数的指针了。
  • 因为是引用,所以必须初始化。

2.4 const限定符

使用const限定符修饰类型别名,容易出错的地方就是对指针的理解。

  • const + 引用,只有常量引用(实际上是指向常量的引用)一种情况。
  • const + 指针,有顶层const和底层const之分。
typedef string *pname;
const pname n1 = new string("Matt"); //顶层const

请问上例const是顶层cosnt还是底层const?
答案:顶层const,即我们不能改变n1的值。

若把原类型带入,很可能得到:

const string *n1 = new string("Matt");
// 这是一个底层const

这是错误的理解。

实际上,pname已经是一个独立的类型了,const修饰的目标是对给定的类型的修饰,就是表示n1不允许被改变。

但是:

const pname *p_n1 = &n1;
// 这是一个底层const

2.5 总结

  • 可以使用类型别名(type alias) 技术,即给某一个类型起一个别名,让复杂的名字变简单,易于理解和使用,帮助程序员清楚地知道使用该类型的目的。
  • 使用typedef关键字或者使用C++11引入的别名声明都可以声明类型别名。
  • 类型别名与复合数据类型在一起使用时,很容易出错。

宏定义(#define)

 #define   宏名   替换文本

在预处理工作过程中,代码中所有出现的“宏名”,都会被“替换文本”替换。这个替换的过程被称为“宏代换”或“宏展开”

#define M 5                // 宏定义
#define PI 3.14            //宏定义
int a[M];                  // 会被替换为: int a[5];
int b = M;                 // 会被替换为: int b = 5;
printf("PI = %.2f\n", PI); // 输出结果为: PI = 3.14

如果要写宏不止一行,则在结尾加反斜线符号使得多行能连接上,如:

#define HELLO "hello the wo\
  rld"
printf("HELLO is %s\n", HELLO);
//输出结果为: HELLO is hello the wo  rld 

1、#define 与 typedef(类型别名) 的区别:

  • 两者都可以用来表示数据类型
// 两者是等效的
#define INT1 int
typedef int INT2;
#define INT1 int *
typedef int * INT2;
INT1 a1, b1;
INT2 a2, b2;
b1 = &m;         //报错
b2 = &n;         // OK

所以两者区别在于,宏定义只是简单的字符串代换,在预处理阶段完成。而typede不是简单的字符串代换,而是可以用来做类型说明符的重命名的,类型的别名可以具有类型定义说明的功能,在编译阶段完成的。

2、有参宏(与函数的区别)

和函数类似,在宏定义中的参数成为形式参数,在宏调用中的参数成为实际参数。

#define N 5                          //无参宏
#define COUNT(M) M * M               //有参宏
printf("COUNT = %d\n", COUNT(10));   // 替换为: COUNT(10) = 10 * 10
                                     // 输出结果: COUNT = 100

所以在宏定义中,有参宏的形参不分配内存单元,所以不作类型定义。而函数 中形参是局部变量,会在栈区分配内存单元,所以要作类型定义,而且实参与形参之间是“值传递”。而宏只是符号代换,不存在值传递。

类型转换

1. 隐式类型转换

隐式类型转换(implicit conversion)
是指无须程序员的介入、甚至不需要程序员了解的一种转换。

int ival = 3.541 + 3;
// 先将3转换成double,然后执行加法得到double类型临时量6.541
// 再将6.541小数部分忽略,得到6,初始化变量ival

编译器只会警告该运算损失了精度。

发生条件

在以下五种情况中,编译器自动执行隐式类型转换:

  1. 大多数表达式中,比int类型小的整形值提升为较大的整数类型(整型提升)
  2. 在条件语句中,非布尔值转换成布尔值(布尔转换
  3. 在初始化过程中,初始值转换成变量的类型;赋值语句中,右侧运算对象转换成左侧运算对象的类型(赋值转换
  4. 算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型(严格匹配
  5. 函数调用

1.1 整型提升(类型提升)

算术转换遵循的一个原则,整型提升:小整型提升到较大整型(发生条件1)。

因为C++标准只规定了各个数据类型的最小尺寸以及几种类型的大小关系,因此提升的结果根据编译器不同。

  • 原则1:小整形bool、char、signed char、unsigned char、short、unsigned short将被提升成int。若int也装不下,就提升为unsigned int。
  • 原则2:较大的char类型wchar_t、char16_t、char32_t提升成int、unsigned int、long unsigned long、long long和unsigned long long中最小的。

看下表:(假设int在某编译器上为32bit)

类型含义尺寸提升
bool布尔类型8位int
char字符8位int
wchar_t宽字符32位int
char16_tUnicode字符16位int
char32_tUnicode字符32位int
short短整型16位int
int整型32位long
long长整型64位long long
long long长整型64位

对于浮点型,也存在类似的提升,float提升到double,double提升至long double。

虽然名称叫做整型提升,但是从整型到浮点型的转换,我们也可以理解为从精度较低整型的提升到精度较高的浮点型,因此有时将这两种转换统称为类型提升

注意:提升不能一步到底:

char c = 'a';
float f = 3.14;
c + f;
// c首先提升成int
// int再转换成float

2.C++四种强制类型转换

在C++语言中新增了四个关键字static_cast、const_cast、reinterpret_cast 和 dynamic_cast。新类型的强制转换可以提供更好的控制强制转换过程。

2.1.static_cast

具有明确定义的类型转换,除了底层const,都可以使用static_cast运算符。

int i, j;
double s = static_cast<double>(j) / i;
// 使用static_cast将整型j转换为double
// i隐式转换为double
// 二者除法,得到double类型的结果,初始化s

2.2.const_cast

static_cast不能改变底层const,可以使用const_cast。

int a = 20;
const int *p_ca = &a;
int *p_a = const_cast<int *>(p_ca);

可以把非底层const的变量赋值给一个底层const

int a = 20;
int *p_a = &a;
// 非底层const赋值给底层const
const int *p_ca = p_a;

但是不可以使用底层const赋值给非底层const

int a = 20;
const int *p_ca = &a;
// 编译出错
// 底层const赋值给非底层const
int *p_a = p_ca;

使用const_cast:

int a = 20;
const int *p_ca = &a;
// 强制转换
int *p_a = const_cast<int *>(p_ca);

2.3.reinterpret_cast

reinterpret_cast为运算对象的位模式提供较低层次上的重新解释。

2.4.dynamic_cast

dynamic_cast的职责主要在具有继承关系的父类与子类之间。

1. dynamic_cast<type *>(expr)   // 指针
2. dynamic_cast<type &>(expr)   // 左值引用
3. dynamic_cast<type &&>(expr)  // 右值引用

3种形式中所有的 type 必须是类类型且应该 含有虚函数

3种形式对应的硬性要求分别是:

  • 指针必须有效
  • expr必须是左值
  • expr不能是左值

满足任一以下条件,转换成功:

  • expr是type类型的公有派生类
  • expr是type类型的公有基类
  • expr是type类型本身

否则转换失败。失败时:

  1. 指针返回0
  2. 引用抛出bad_cast异常

使用dynamic_cast最好进行if判断和异常获取,如下面的指针和引用的例子。

命名空间特点

  • 命名空间既可以定义在全局作用域内,也可以定义在其它命名空间中,但是不能定义在函数或类的内部。
  • 命名空间的名字必须在定义它的作用域内保持唯一;
  • 命名空间作用域后面无须分号,即命名空间定义时最后面的花括号后面无需分号。
  • 命名空间可以不连续。即,可以在多个文件中定义同一个命名空间,需要保证所有成员的名字不同;
  • 命名空间可以嵌套定义。即命名空间内部可以定义另一个命名空间。
//定义一个名称空间F,并在里面添加两个函数start stop
namespace F {
    void start(){}
    void stop(){}
}
namespace G {
    void start(){}
    void stop(){}
}
//我们也可以在std名称空间中添加类,虽然一般不这么做,这里仅仅用于帮助我们理解什么是名称空间
namespace std {
    struct Student{};
}
int main()
{
    F::start();//通过名称空间名称F访问该名称空间中的函数start,下同
    F::stop();

    G::start();
    G::stop();

    std::Student my;

    using namespace F;//将名称空间F中的名称暴露出来,不需要使用F::也可以访问
    start();//再次执行名称空间F中的start函数

    return 0;
}

名称空间std
std表示standard template library。C++ 标准模板库。C++为了方面我们开发会提供很多对象,函数放在其中给我们使用。cout 与 cin就在其中。

using

  • 每次使用名称空间的对象都加上名称空间的名字,比如std::cout,的确也麻烦。
  • 这时候可以使用using namespace std;来直接使用cout对象(程序会到std中找cout)
  • 参考上面代码中的using namespace F;