1、C++基础

目录

计算机语言发展

最初的计算机语言(机器语言) --> 汇编语言 -->高级语句

最初的计算机语言(机器语言)

  • 由二进制代码构成
  • 计算机硬件可以识别
  • 可以表示简单的操作
  • 例如:加法、减法、数据移动等等

汇编语言

  • 将机器指令映射为助记符
  • 如 ADD、SUB、mov 等;
  • 抽象层次低,需要考虑机器细节。

高级语言

  • 关键字、语句容易理解;
  • 有含义的数据命名和算式;
  • 抽象层次较高;
    例如,算式:a+b+c/d
  • 屏蔽了机器的细节;
    例如,这样显示计算结果:cout<<a+b+c/d

1 C++和C语言的血缘关系

现在看来,C++ 和C语言虽然是两门独立的语言,但是它们却有着扯也扯不清的关系。

早期并没有“C++”这个名字,而是叫做“带类的C”。“带类的C”是作为C语言的一个扩展和补充出现的,它增加了很多新的语法,目的是提高开发效率,如果你有 Java Web 开发经验,那么你可以将它们的关系与 Servlet 和 JSP 的关系类比。

这个时期的 C++ 非常粗糙,仅支持简单的面向对象编程,也没有自己的编译器,而是通过一个预处理程序(名字叫 cfront),先将 C++ 代码”翻译“为C语言代码,再通过C语言编译器合成最终的程序。

随着 C++ 的流行,它的语法也越来越强大,已经能够很完善的支持面向过程编程、面向对象编程(OOP)和泛型编程,几乎成了一门独立的语言,拥有了自己的编译方式。

我们很难说 C++ 拥有独立的编译器,例如 Windows 下的微软编译器(cl.exe)、Linux 下的 GCC 编译器、Mac 下的 Clang 编译器(已经是 Xcode 默认编译器,雄心勃勃,立志超越 GCC),它们都同时支持C语言和 C++,统称为 C/C++ 编译器。对于C语言代码,它们按照C语言的方式来编译;对于 C++ 代码,就按照 C++ 的方式编译。

从表面上看,C、C++ 代码使用同一个编译器来编译,所以上面我们说“后期的 C++ 拥有了自己的编译方式”,而没有说“C++ 拥有了独立的编译器”。

2 C++类和对象到底是什么意思?

C++ 是一门面向对象的编程语言,理解 C++,首先要理解类(Class)和对象(Object)这两个概念。

C++ 中的类(Class)可以看做C语言中结构体(Struct)的升级版。结构体是一种构造类型,可以包含若干成员变量,每个成员变量的类型可以不同;可以通过结构体来定义结构体变量,每个变量拥有相同的性质。

例如:

#include <stdio.h>

//定义结构体 Rect
struct Rect{
	int xmin;
	int ymin;
	int xmax;
	int ymax;
};

//显示结构体的成员变量
void printRect(struct Rect rect)
{
	printf("xmin:%d, ymin:%d, xmax:%d, ymax:%d\n", rect.xmin, rect.ymin, 
	rect.xmax, rect.ymax);
}

int main()
{
    struct Rect rect;
    //为结构体的成员变量赋值
    rect.xmin = 0;
	rect.ymin = 10;
	rect.xmax = 50;
	rect.ymax = 60;
    //调用函数
    printRect(rect);
    return 0;
}

//运行结果:
xmin:0, ymin:10, xmax:50, ymax:60

C++ 中的类也是一种构造类型,但是进行了一些扩展,类的成员不但可以是变量,还可以是函数;通过类定义出来的变量也有特定的称呼,叫做“对象”。
例如:

#include <stdio.h>
//通过class关键字类定义类
class Rect{
public:
    //类包含的变量
    int xmin;
	int ymin;
	int xmax;
	int ymax; 
    //类包含的函数
    void say()
    {
       printf("xmin:%d, ymin:%d, xmax:%d, ymax:%d\n", rect.xmin, rect.ymin, rect.xmax, rect.ymax);
    }
};
int main(){
    //通过类来定义变量,即创建对象
    Rect rect;  
    //为类的成员变量赋值
    rect.xmin = 0;
	rect.ymin = 10;
	rect.xmax = 50;
	rect.ymax = 60;
    //调用类的成员函数
    rect.say();
    return 0;
}

运行结果与上例相同。

结构体和类都可以看做一种由用户自己定义的复杂数据类型,在C语言中可以通过结构体名来定义变量,在 C++ 中可以通过类名来定义变量。不同的是,通过结构体定义出来的变量还是叫变量,而通过类定义出来的变量有了新的名称,叫做对象(Object)。

在 C++ 中,通过类名就可以创建对象,这个过程叫做类的实例化,因此也称对象是类的一个实例(Instance)。有些资料也将类的成员变量称为属性(Property),将类的成员函数称为方法(Method)。

面向对象编程(Object Oriented Programming,OOP)

类是一个通用的概念,C++、Java、C#、PHP 等很多编程语言中都支持类,都可以通过类创建对象。可以将类看做是结构体的升级版。

因为 C++、Java、C#、PHP 等语言都支持类和对象,所以使用这些语言编写程序也被称为面向对象编程,这些语言也被称为面向对象的编程语言。C语言因为不支持类和对象的概念,被称为面向过程的编程语言。

在C语言中,我们会把重复使用或具有某项功能的代码封装成一个函数,将拥有相关功能的多个函数放在一个源文件,再提供一个对应的头文件,这就是一个模块。使用模块时,引入对应的头文件就可以。

而在 C++ 中,多了一层封装,就是类(Class)。类由一组相关联的函数、变量组成,你可以将一个类或多个类放在一个源文件,使用时引入对应的类就可以。下面是C和C++项目组织方式的对比:

不要小看类(Class)这一层封装,它有很多特性,极大地方便了中大型程序的开发,它让 C++ 成为面向对象的语言。

面向对象编程在代码执行效率上绝对没有任何优势,它的主要目的是方便程序员组织和管理代码,快速梳理编程思路,带来编程思想上的革新

面向对象编程是针对开发中大规模的程序而提出来的,目的是提高软件开发的效率。不要把面向对象和面向过程对立起来,面向对象和面向过程不是矛盾的,而是各有用途、互为补充的。

面向对象特点

封装

  • 隐蔽对象的内部细节;
  • 对外形成一个边界;
  • 只保留有限的对外接口;
  • 使用方便、安全性好。

继承

  • 意义在于软件复用;
  • 改造、扩展已有类形成新的类。

多态

  • 同样的消息作用在不同对象上,可以引起不同的行为。

3 c++编译与运行过程

C/C++ 代码生成可执行文件的过程:

C++源文件的后缀

C语言源文件的后缀非常统一,在不同的编译器下都是.c。C++ 源文件的后缀则有些混乱,不同的编译器支持不同的后缀,下表是一个简单的汇总:

编辑器 Microsoft Visual C++ GCC(GNU C++) Borland C++ UNIX
后缀 cpp、cxx、cc cpp、cxx、cc、c++、C cpp C、cc、cxx

UNIX 是昂贵的商业操作系统,初学者几乎用不到;Microsoft Visual C++ 是微软的 C/C++ 编译器,VC 6.0、VS 都使用该编译器。推荐使用.cpp作为 C++ 源文件的后缀。

4 命名空间和头文件

4.1命名空间

4.1.1 命名空间作用

目的:避免命名冲突

namespace 是C++中的关键字,用来定义一个命名空间,语法格式为:

namespace name{
    //variables, functions, classes
}

name是命名空间的名字,它里面可以包含变量、函数、类、typedef、#define 等.

使用变量、函数时要指明它们所在的命名空间。例如:


namespce deploy{
    std::string name;
}

namespace mmdeploy{
    std::string name;
}

deploy::name = std::string("tensorrt");
mmdeploy::name = std::string("openvion");

::是一个新符号,称为域解析操作符,在C++中用来指明要使用的命名空间。

除了直接使用域解析操作符,还可以采用 using 关键字声明

using deploy::name;
name = std::string("tensorrt");
mmdeploy::name = std::string("openvion");

在代码的开头用using声明了 deploy::name,它的意思是,using 声明以后的程序中如果出现了未指明命名空间的 name,就使用 deploy::name;但是若要使用mmdeploy定义的 name,仍然需要 mmdeploy::name。

using 声明不仅可以针对命名空间中的一个变量,也可以用于声明整个命名空间

using namespace deploy;

4.1.2 std命名空间

C++ 是在C语言的基础上开发的,早期的 C++ 还不完善,不支持命名空间,没有自己的编译器,而是将 C++ 代码翻译成C代码,再通过C编译器完成编译。这个时候的 C++ 仍然在使用C语言的库,stdio.h、stdlib.h、string.h 等头文件依然有效;此外 C++ 也开发了一些新的库,增加了自己的头文件,例如:
iostream.h:用于控制台输入输出头文件。
fstream.h:用于文件操作的头文件。
complex.h:用于复数计算的头文件。

和C语言一样,C++ 头文件仍然以.h为后缀,它们所包含的类、函数、宏等都是全局范围的。

后来 C++ 引入了命名空间的概念,计划重新编写库,将类、函数、宏等都统一纳入一个命名空间,这个命名空间的名字就是std。std 是 standard 的缩写,意思是“标准命名空间”

4.2 头文件

但是这时已经有很多用老式 C++ 开发的程序了,它们的代码中并没有使用命名空间,直接修改原来的库会带来一个很严重的后果:程序员会因为不愿花费大量时间修改老式代码而极力反抗,拒绝使用新标准的 C++ 代码。

C++ 开发人员想了一个好办法,保留原来的库和头文件,它们在 C++ 中可以继续使用,然后再把原来的库复制一份,在此基础上稍加修改,把类、函数、宏等纳入命名空间 std 下,就成了新版 C++ 标准库。这样共存在了两份功能相似的库,使用了老式 C++ 的程序可以继续使用原来的库,新开发的程序可以使用新版的 C++ 库。

为了避免头文件重名,新版 C++ 库也对头文件的命名做了调整,去掉了后缀.h,所以老式 C++ 的iostream.h变成了iostream,fstream.h变成了fstream。而对于原来C语言的头文件,也采用同样的方法,但在每个名字前还要添加一个c字母,所以C语言的stdio.h变成了cstdio,stdlib.h变成了cstdlib

下面是总结的 C++ 头文件的现状:

  1. 旧的 C++ 头文件,如 iostream.h、fstream.h 等将会继续被支持,尽管它们不在官方标准中。这些头文件的内容不在命名空间 std 中。

  2. 新的 C++ 头文件,如 iostream、fstream 等包含的基本功能和对应的旧版头文件相似,但头文件的内容在命名空间 std 中。
    注意:在标准化的过程中,库中有些部分的细节被修改了,所以旧的头文件和新的头文件不一定完全对应。

  3. 标准C头文件如 stdio.h、stdlib.h 等继续被支持。头文件的内容不在 std 中。

  4. 具有C库功能的新C++头文件具有如 cstdio、cstdlib 这样的名字。它们提供的内容和相应的旧的C头文件相同,只是内容在 std 中。

可以发现,对于不带.h的头文件,所有的符号都位于命名空间 std 中,使用时需要声明命名空间 std;对于带.h的头文件,没有使用任何命名空间,所有符号都位于全局作用域。这也是 C++ 标准所规定的。

5 C++字符集和词法记号

5.1字符集

  • 大小写的英文字母:A~Z,a~z

  • 数字字符:0~9

  • 特殊字符:

    ! # % ^ & * _ + =

    • ~ < > / \ ‘ “ ; .
      , : ? ( ) [ ] { } |

5.2词法记号

  • 关键字
    C++预定义的单词

  • 标识符
    程序员声明的单词,它命名程序正文中的一些实体

  • 文字
    在程序中直接使用符号表示的数据

  • 分隔符 () {} , : ;
    用于分隔各个词法记号或程序正文

  • 运算符(操作符)
    用于实现各种运算的符号

  • 空白符
    空格、制表符(TAB键产生的字符)、垂直制表符、换行符、回车符和注释的总称

5.3标识符的构成规则

  • 以大写字母、小写字母或下划线(_)开始。

  • 可以由以大写字母、小写字母、下划线(_)或数字0~9组成。

  • 大写字母和小写字母代表不同的标识符。

  • 不能是C++关键字或操作符。

6 基本数据类型、常量、变量、运算

6.1 C++能够处理的基本数据类型

  • 整数类型
  • 浮点数类型
  • 字符类型
  • 布尔类型

6.2 程序中的数据

  • 常量
  1. 在源程序中直接写明的数据;
  2. 其值在整个程序运行期间不可改变。
  • 变量

    • 在程序运行过程中允许改变的数据。
  • 数据的存储方式

机器中实际存储的是原始数据的补码,因为补码方便进行数学运算,这里就会涉及到三个概念: 原码,反码,补码。下面的例子中,假设某台机器的int和unsigned int都占用4个字节,即32位。

  • 原码:正数的原码为其二进制表示;负数的原码为其绝对值的二进制表示,并将最高位置为1,表示其符号。
  • 反码:正数的反码与原码相同;负数的反码为其原码除符号位外,其他位按位取反。
  • 补码:正数的补码与原码相同;负数的补码为其反码+1。
示例:
3的原码:0000 0000 0000 0000 0000 0000 0000 0011
3的反码:0000 0000 0000 0000 0000 0000 0000 0011
3的补码:0000 0000 0000 0000 0000 0000 0000 0011
-3的原码:1000 0000 0000 0000 0000 0000 0000 0011
-3的反码:1111 1111 1111 1111 1111 1111 1111 1100
-3的补码:1111 1111 1111 1111 1111 1111 1111 1101

6.3 数据类型

6.3.1 整数类型

基本的整数类型:int

  • 按符号分
  1. 符号的(signed)

  2. 无符号的(unsigned)

  • 按照数据范围分
  1. 短整数(short)

  2. 长整数(long)

  3. 长长整数( long long )

  • ISO C++标准并没有明确规定每种数据类型的字节数和取值范围,它只是规定它们之间的字节数大小顺序满足:

(signed/unsigned)signed char ≤(unsigned) short int ≤(unsigned) int ≤(unsigned) long int ≤ long long int

6.3.2 字符类型(char)

使用一个8位的整数

  1. 容纳单个字符的编码;
  2. 实质上存储的也是整数。

中文字符使用16位或者32位

char16_t c4 = u'于';
char32_t c5 = u'于;

6.3.3浮点数类型

1.单精度(float)(32bit)
存储格式

  • [0, 1]有多少数值
    无穷多个

  • 32bit可以表达多少数值
    $ 2^{32}$

  • 如果你想要1.2, 但float仅能够提供1.200000047...;就是1.2实际上无法表达的

2.双精度(double)64bit

3.扩展精度(long double)
如果128 bits支持,64bits

理解计算?

  • 浮点数操作总是带来一些微小误差
  • 这些误差不能被消除
  • 我们需要做的:管理这些误差

4.精度

float f1 = 2.34E+10f;
float f2 = f1+10;  // but f2 = f1

cout.setf(ios_base::fixed, ios_base::floatfield);
cout<<"f1-f2="<<f1-f2<<endl;
cout<<"(f1-f2==0)="<<(f1-f2==0)<<endl;

f1-f2=0.00000
(f1-f2==0)=1

为什么会这样?
1.采样精度不够

5.float值比较操作
采用==操作

if(f1 ==f2)  //bad

if(fabs(f1-f2)<FLT_EPSILON)  //good

6. inf and nan
inf表示无穷大
nan表

6.3.4 字符串类型

1.有字符串常量

2.基本类型中没有字符串变量

3.采用字符数组存储字符串(C风格的字符串)

4.标准C++类库中的String类(C++风格的字符串)

6.3.5布尔类型(bool)

  1. 只有两个值:true(真) 、false(假)

  2. 常用来表示关系比较、相等比较或逻辑运算的结果

  #define true 1 
  #define false 0

在C99以后 定义在 stdbool.h文件中

#include<stdbool.h>

6.3.6 size_t

  • 无符号整数 unsinged int
  • 返回类型和sizeof相同
  • 存储任何对象最大值
  • 32-bit,or 64-bit

6.3.7 修复int数据类型

在c++11以后定义在

  • int8_t
  • int16_t
  • int32_t
  • int64_t
  • uint8_t
  • uint16_t
  • uint32_t
    ...

判断是否数据溢出可以使用宏定义
INT8_MIN
INT16_MIN
INT32_MIN
INT64_MIN
INT8_MAX
INT16_MAX
INT32_MAX
INT64_MAX

6.3.8 auto

  • auto任意的类型
    具体类型将在初始化时定义
auto a = 2; //type of a is int 
auto bc=2.3; //type of b is double
auto c; // valid in C, error in C++
auto d = a*1.2; //type of d is double
  • 问题
auto a = 2; //type of a is int

//will a be converted to a
// double type variable?
a = 2.3;
// No! 2.3 will be converted to a int 2, then assigned to a

各基本类型的取值范围

6.4 常量(const)

6.4.1 常变量

使用const关键字,语法格式为:

const 数据类型 变量名;

例如:

const int m = 10;

6.4.2 C++中的 const 更像编译阶段的 #define

在C语言中,const 用来限制一个变量,表示这个变量不能被修改,我们通常称这样的变量为常量(Constant),在C++中,const的含义并没有改变,只是对细节进行了一些调整.

先来看下面的两条语句:

const int m = 10;
int n = m;

我们知道,变量是要占用内存的,即使被 const 修饰也不例外。m、n 两个变量占用不同的内存,int n = m;表示将 m 的值赋给 n,这个赋值的过程在C和C++中是有区别的。

在C语言中,编译器会先到 m 所在的内存取出一份数据,再将这份数据赋给 n;而在C++中,编译器会直接将 10 赋给 n,没有读取内存的过程,和int n = 10;的效果一样。C++ 中的常量更类似于#define命令,是一个值替换的过程,只不过#define是在预处理阶段替换,而常量是在编译阶段替换。

C++ 对 const 的处理少了读取内存的过程,优点是提高了程序执行效率,缺点是不能反映内存的变化,一旦 const 变量被修改,C++ 就不能取得最新的值。

有读者提出疑问,const 变量不是禁止被修改吗?对,这种说法没错!不过这只是语法层面上的限制,通过指针仍然可以修改。下面的代码演示了如何通过指针修改 const 变量:

#include <stdio.h>
int main(){
    const int n = 10;
    int *p = (int*)&n;  //必须强制类型转换
    *p = 99;  //修改const变量的值
    printf("%d\n", n);
    return 0;
}

注意,&n得到的指针的类型是const int *,必须强制转换为int *后才能赋给 p,否则类型是不兼容的。

将代码放到.c文件中,以C语言的方式编译,运行结果为99。再将代码放到.cpp文件中,以C++的方式编译,运行结果就变成了10。这种差异正是由于C和C++对 const 的处理方式不同造成的。

在C语言中,使用 printf 输出 n 时会到内存中获取 n 的值,这个时候 n 所在内存中的数据已经被修改成了 99,所以输出结果也是 99。而在C++中,printf("%d\n", n);语句在编译时就将 n 的值替换成了 10,效果和printf("%d\n", 10);一样,不管n 所在的内存如何变化,都不会影响输出结果。

当然,这种修改常量的变态代码在实际开发中基本不会出现,本例只是为了说明C和C++对 const 的处理方式的差异:C语言对 const 的处理和普通变量一样,会到内存中读取数据;C++ 对 const 的处理更像是编译时期的#define,是一个值替换的过程

6.4.3 全局const变量作用范围

我们知道,普通全局变量的作用域是当前文件,但是在其他文件中也是可见的,使用extern声明后就可以使用

代码段1(源文件1):

#include <stdio.h>
int n = 10;
void func();
int main(){
    func();
    printf("main: %d\n", n);
    return 0;
}

代码段2(源文件2):

#include <stdio.h>
extern int n;
void func(){
    printf("module: %d\n", n);
}

不管是以C还是C++的方式编译,运行结果都是:
module: 10
main: 10

由于 C++ 中全局 const 变量的可见范围仅限于当前源文件,所以可以将它放在头文件中,这样即使头文件被包含多次也不会出错,请看下面的例子。

module.h 代码:

#pragma once
const int n = 10;
void func();

module.cpp 代码:

#include <stdio.h>
#include "module.h"
void func(){
    printf("module: %d\n", n);
}

main.cpp 代码:

#include <stdio.h>
#include "module.h"
int main(){
    func();
    printf("main: %d\n", n);
    return 0;
}

运行结果:
module: 10
main: 10

C和C++中全局 const 变量的作用域相同,都是当前文件,不同的是它们的可见范围:C语言中 const 全局变量的可见范围是整个程序,在其他文件中使用 extern 声明后就可以使用;而C++中 const 全局变量的可见范围仅限于当前文件,在其他文件中不可见,所以它可以定义在头文件中,多次引入后也不会出错。

6.4.4 const在指针不同区别

常量指针

又叫常指针,可以理解为常量的指针,也即这个是指针,但指向的是个常量,这个常量是指针的值(地址),而不是地址指向的值。

关键点:
1.常量指针指向的对象不能通过这个指针来修改,可是仍然可以通过原来的声明修改;
2.常量指针可以被赋值为变量的地址,之所以叫常量指针,是限制了通过这个指针修改变量的值;
3.指针还可以指向别处,因为指针本身只是个变量,可以指向任意地址;

代码形成:

const int* p_int;  int const* p_int; 

指针常量

定义:
本质是一个常量,而用指针修饰它。指针常量的值是指针,这个值因为是常量,所以不能被赋值。

关键点:
1.它是个常量!
2.指针所保存的地址可以改变,然而指针所指向的值却不可以改变;
3.指针本身是常量,指向的地址不可以变化,但是指向的地址所对应的内容可以变化;

代码形式:

int a =0;
int b =0;
//-------常量指针-------
const int *p1 = &a;
a = 300;     //OK,仍然可以通过原来的声明修改值,
//*p1 = 56;  //Error,*p1是const int的,不可修改,即常量指针不可修改其指向地址
p1 = &b;     //OK,指针还可以指向别处,因为指针只是个变量,可以随意指向;

//-------指针常量-------//
int*  const p2 = &a;
a = 500;     //OK,仍然可以通过原来的声明修改值,
*p2 = 400;   //OK,指针是常量,指向的地址不可以变化,但是指向的地址所对应的内容可以变化
//p2 = &b;     //Error,因为p2是const 指针,因此不能改变p2指向的内容

//-------指向常量的常量指针-------//
const int* const p3 = &a;
//*p3 = 1;    //Error
//p3 = &b;    //Error
a = 5000;    //OK,仍然可以通过原来的声明修改值

指向常量的常指针

定义:
   指向常量的指针常量就是一个常量,且它指向的对象也是一个常量。

关键点:

1.一个指针常量,指向的是一个指针对象;
2.它指向的指针对象且是一个常量,即它指向的对象不能变化;

代码形式:

const int* const p;

那如何区分这几类呢?

带两个const的肯定是指向常量的常指针,很容易理解,主要是如何区分常量指针和指针常量:

一种方式是看 * 和 const 的排列顺序,比如

     int const* p; //const * 即常量指针
     const int* p; //const * 即常量指针
     int* const p; //* const 即指针常量

还一种方式是看const离谁近,即从右往左看,比如

    int const* p; //const修饰的是*p,即*p的内容不可通过p改变,但p不是const,p可以修改,*p不可修改;
    const int* p; //同上
    int* const p; //const修饰的是p,p是指针,p指向的地址不能修改,p不能修改,但*p可以修改;

举例:

 //-------常量指针-------
    int a = 0;
    int b = 0;
    const int *p1 = &a;

    std::cout<<"*p1:"<< *p1<<std::endl;
    a = 300; //OK,仍然可以通过原来的声明修改值
    std::cout<<"*p1:"<< *p1<<std::endl;
    //*p1 = 56;  //Error,*p1是const int的,不可修改,即常量指针不可修改其指向地址
    p1 = &b;
    std::cout<<"*p1:"<<*p1<<std::endl;

    //-------指针常量-------//
    a = 0;
    b = 0;
    int*  const p2 = &a;
    std::cout<<"*p2:"<< *p2<<std::endl;
    a = 500;     //OK,仍然可以通过原来的声明修改值,
    std::cout<<"*p2:"<< *p2<<std::endl;
    *p2 = 400;   //OK,指针是常量,指向的地址不可以变化,但是指向的地址所对应的内容可以变化
    std::cout<<"*p2:"<< *p2<<std::endl;
    //p2 = &b;     //Error,因为p2是const 指针,因此不能改变p2指向的内容

    //-------指向常量的常量指针-------//
    a = 0;
    b = 0;
    const int* const p3 = &a;
    std::cout<<"*p3:"<<*p3<<std::endl;
    //*p3 = 1;    //Error
    //p3 = &b;    //Error
    a = 5000;    //OK,仍然可以通过原来的声明修改值

    std::cout<<"*p3:"<<*p3<<std::endl;
    输出:
    *p1:0
    *p1:300
    *p1:0
    *p2:0
    *p2:500
    *p2:400
    *p3:0
    *p3:5000

在函数中通过以下方式保证传入内容不被修改

void func(const int *);
void func(const int &);

6.5 运算

6.5.1 算术运算

  • 基本算术运算符

    1.+ - * /(若整数相除,结果取整)

    2.%(取余,操作数为整数)

  • 优先级与结合性

    • 先乘除,后加减,同级自左至右
  • ++, --(自增、自减)

    • 例:i++; --j;

6.5.2 赋值运算

1.定义:将值赋给变量

2.赋值运算符“=”

3.赋值表达式:

  • 用赋值运算符连接的表达式
    例如: n=5
    n = n + 5

  • 表达式的值
    赋值运算符左边对象被赋值后的值

  • 表达式的类型
    赋值运算符左边对象的类型

4.复合的赋值运算符

  • +=,-=,*=,/=,%=,<<=,>>=,&=,^=,|=

a += 3 等价于 a = a + 3
x \*= y + 8 等价于 x = x \* (y + 8)

6.5.3 逗号运算和逗号表达式

  • 格式

    表达式1,表达式2;

  • 求解顺序及结果

    • 先求解表达式1,再求解表达式2

    • 最终结果为表达式2的值

例如: a = 3 * 5 , a * 4; 最终结果为60

6.5.4 关系运算与关系表达式

  • 关系运算是比较简单的一种逻辑运算,优先次序为:

  • 关系表达式是一种最简单的逻辑表达式

    • 其结果类型为 bool,值只能为 true 或false。
      例如:a > b,c <= a + b,x + y == 3

6.5.5 逻辑运算与逻辑表达式

1.逻辑运算符

        !(非)      &&(与)        ||(或)

优先次序: 高         →           低

逻辑运算结果类型:bool,值只能为 true 或false

2.逻辑表达式
例如:(a > b) && (x > y)

2.1 “&&”的运算规则

  • 两侧表达式都为真,结果为真;

  • 有一侧表达式为假,结果为假。

2.2 “&&” 的“短路特性”

表达式1 && 表达式2

  • 先求解表达式1

  • 若表达式1的值为false,则最终结果为false,不再求解表达式2

若表达式1的结果为true,则求解表达式2,以表达式2的结果作为最终结果

2.3 “||”的运算规则

  • 两侧表达式都为假,结果为假;

  • 有一侧表达式为真,结果为真。

2.4 “||” 的“短路特性”

表达式1 || 表达式2

  • 先求解表达式1

  • 若表达式1的值为true,则最终结果为true,不再求解表达式2

若表达式1的结果为false,则求解表达式2,以表达式2的结果作为最终结果

6.5.6 条件运算符与条件表达式

1.一般形式

  • 表达式1?表达式2:表达式3

表达式1 必须是bool 类型

2.执行顺序

  • 先求解表达式1,

  • 若表达式1的值为true,则求解表达式2,表达式2的值为最终结果

若表达式1的值为false,则求解表达式3,表达式3的值为最终结果

3.条件运算符优先级高于赋值运算符,低于逻辑运算符


表达式1是bool类型,表达式2、3的类型可以不同,条件表达式的最终类型为2 和3 中较高的类型。

6.5.6 sizeof运算、位运算

sizeof运算

1.语法形式
sizeof (类型名) 或 sizeof 表达式

2.结果值:
“类型名”所指定的类型,或“表达式”的结果类型所占的字节数。

例:

sizeof(short)

sizeof  x

位运算——按位与(&)

1.运算规则

将两个运算量的每一个位进行逻辑与操作

2.举例:计算3 & 5

3.用途:

  • 将某一位置0,其他位不变。
    例如:将char型变量a的最低位置0: a = a & 0xfe; ;(0xfe:1111 1110)

  • 取指定位。
    例如:有char c; int a; 取出a的低字节,置于c中:c=a & 0xff; (0xff:1111 1111)

位运算——按位或(|)

1.运算规则
将两个运算量的每一个位进行逻辑或操作

2.举例:计算3 | 5

3.用途:

  • 将某些位置1,其他位不变。
    例如:将 int 型变量 a 的低字节置 1 :
    a = a | 0xff;

位运算——按位异或(^)

1.运算规则

  • 两个操作数进行异或:
    若对应位相同,则结果该位为 0,
    若对应位不同,则结果该位为 1,

2.举例:计算071^052

3.用途举例:使特定位翻转(与0异或保持原值,与1异或取反)

例如:要使 01111010 低四位翻转:

位运算——取反(~)

1.运算规则

单目运算符,对一个二进制数按位取反。

2.例:
025:0000000000010101

~025:1111111111101010

位运算——移位(<<、>>)

1.左移运算(<<)

左移后,低位补0,高位舍弃。

2.右移运算(>>)

右移后:

低位:舍弃

高位:无符号数:补0; 有符号数:补“符号位”

7混合运算时数据类型的转换——显式转换

int num_int1 = 9;
int num_int2 = 'C'; // implicit conversion(隐式转换)
int num_int3 = (int)'C'; // explicit conversion(显示转换), C-style
int num_int4 = int('C'); //explicit conversion, function style

int num_int5 = 2.8; //implicit conversion 保留整数
float num_float = 2.3; //implicit conversion
short num_short = 650000; //

1.显式类型转换的作用是将表达式的结果类型转换为类型说明符所指定的类型。

2.语法形式

  • 类型说明符(表达式)

  • (类型说明符)表达式

  • 类型转换操作符<类型说明符>(表达式)

类型转换操作符可以是:

const_cast、dynamic_cast、reinterpret_cast、static_cast

3.例如:int(z), (int)z, static_cast<int>(z)

7宏定义

7.1宏定义的概念

(1)简单的宏定义

1. #define <宏名>  <字符串>
2. 例: #define PI 3.1415926

(2)带参数的宏定义

1. #define <宏名> (<参数表>) <宏体>
2. 例: #define A(x) x

一个标识符被宏定义后,该标识符便是一个宏名。这时,在程序中出现的是宏名,在该程序被编译前,先将宏名用被定义的字符串替换,这称为宏替换,替换后才进行编译,宏替换是简单的替换。

7.2宏替换发生的时机

为了能够真正理解#define的作用,让我们来了解一下对C语言源程序的处理过程。当我们在一个集成的开发环境如Turbo C中将编写好的源程序进行编译时,实际经过了预处理、编译、汇编和连接几个过程。其中预处理器产生编译器的输出,它实现以下的功能:
(1)文件包含
可以把源程序中的#include 扩展为文件正文,即把包含的.h文件找到并展开到#include 所在处。
(2)条件编译
预处理器根据#if和#ifdef等编译命令及其后的条件,将源程序中的某部分包含进来或排除在外,通常把排除在外的语句转换成空行。
(3)宏展开
预处理器将源程序文件中出现的对宏的引用展开成相应的宏 定义,即本文所说的#define的功能,由预处理器来完成。
经过预处理器处理的源程序与之前的源程序有所有不同,在这个阶段所进行的工作只是纯粹的替换与展开,没有任何计算功能,所以在学习#define命令时只要能真正理解这一点,这样才不会对此命令引起误解并误用。

7.3 宏定义中的三个特殊符号:#,##,#@

1. #define Conn(x,y) x##y
2. #define ToChar(x) #@x
3. #define ToString(x) #x

(1)x##y表示什么?表示x连接y
举例说:

  1. int n = Conn(123,456); /* 结果就是n=123456;*/
  2. char* str = Conn("asdf", "adf"); /* 结果就是 str = "asdfadf"; */

(2)再来看#@x,其实就是给x加上单引号,结果返回是一个const char

char a = ToChar(1);结果就是a='1';
做个越界试验char a = ToChar(123);结果就错了;
但是如果你的参数超过四个字符,编译器就给给你报错了!
error C2015: too many characters in constant :P

(3)最后看看#x,估计你也明白了,他是给x加双引号

char* str = ToString(123132);就成了str="123132";

参考资料

1、C++入门教程,C++基础教程
2、快速学习C和C++,基础语法和优化策略,学了不再怕指针(南科大计算机系原版)

posted @ 2023-02-13 13:34  qianglearning  阅读(222)  评论(0)    收藏  举报