C++补基础

该博客参考《新标准C++程序设计教程》,介绍C++编程知识。涵盖计算机基础,如二进制、十六进制及C++历史;讲解基本要素、控制结构、函数、数组、字符串、指针等内容;还提及自定义数据类型,如结构、联合、枚举等,以及程序设计思想和结构。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本篇博客主要参考《新标准C++程序设计教程》2012 年 8 月第一版

#include<iostream>
#include<algorithm>
#include<stdlib.h>
#include<cstring>
#include<string>
#include<cmath>
#include<ctype>
#include<queue>
using namespace std;

const  int  a = 5;     //2.3详解
#define MAX_NUM 100   //2.3详解
 
struct Student {
	string name;
	int age;
	float fGPA;
};

union  联合名 {
	类型名  成员变量名1;  //eg. int age;
	类型名  成员变量名2;
	……
};

 void Function2(int x, int y = Max(a,b), int z = a * b) {
 
}

int n = 4;
int & SetValue() {
	return n;
}

int main() {
	freopen("D:\\a-日常文档使用\\输入数据.txt", "r", stdin); 
	int & r = n; 

	enum Weekday{Mon, Tue, Wed, Thu, Fri, Sat, Sun};
	Weekday workingDay = Sun;
	
	return 0;
}

第一章 计算机基础知识

1.1 信息在计算机中的表示和存储

1.1.1 二进制和十六进制

1、 十六进制数的一位正好对应二进制数的四位。

【 应用举例】:

//leetcode 342. 4的幂
//判断num是不是4的幂次方
class Solution {
  public boolean isPowerOfFour(int num) {
  // 特判 && 2的幂次方 && 2的偶数次方
    return (num > 0) && ((num & (num - 1)) == 0) && ((num & 0xaaaaaaaa) == 0);
  }
}

1.1.2 整数和小数的计算机表示

(1)负整数:
将负整数的所有位取反,再 + 1 就可以得到该数的绝对值。
eg.对于十六进制 0x f f f f f f f f(也就是二进制数是 32个1),符号位为 1,说明该数是负数,此时将0x f f f f f f f f 按位取反再+1得到 0x 0 0 0 0 0 0 0 1,说明这个数绝对值为 1 ,而数本身是 - 1.

(2)、小数分类:定点数 / 浮点数
①定点数:就是规定好了整数部分的位数和小数部分的位数,相当于小数点的位置是规定好了的。
例如 ,用一个32位的二进制定点数表示小数,可以事先规定整数部分是左边的16位(高16位),小数部分是右边的16位(低16位),那么这个二进制数里面就不用包含小数点位置的信息。
用定点数表示小数,由于总位数有限,如果要保证整数部分能表示很大范围(如规定整数部分占30位),那么小数部分的精度(即小数点后面的有效数字位数)就会不足;如果希望小数部分的精度很高(如规定小数部分占30位),那么又会导致整数部分能表示的数值范围太小。
定点数使用不便,因此大多数计算机系统中都是用“浮点数"来表示小数。
②浮点数中包含了小数点位置的信息,小数点的位置是可变的,所以称为“浮点数”。
浮点数表示法: 我们知道有“科学计数法”,即把数表示为M * 10 ^ E形式。例如,1653可以表示为1.653 * 10 ^ 3或16. 53 * 10 ^ 2, 0. 03可以表示成3 * 10 ^ -2。由于计算机用二进制表示信息,因此将数表示为M * 2 ^ E的形式,用计算机处理更加方便。实际上,浮点数就是M * 2 ^ E形式的的数,其中M称为尾数,E称为阶码,尾数和阶码都是整数。在台计算机中,尾数M和阶码E的位数都是规定好的,M代表了浮点数的有效数字,其位数越多,浮点数的精度就越高;而E确定了浮点数的小数点的位置,E的位数越多,浮点数能表示的数的范围就越大。
计算机中的一个浮点数,其比特数是固定的,例如一共只有 64比特,那么显然该浮点数能表示的数的个数就是有限的。浮点数所能表示的数的范围是由其所能表示的最大值和最小值
决定的,但并不是在此范围内的每个数都能被表示出来。
由于尾数和阶码都可能为负数,因此,浮点数中还应当包含尾数和阶码的符号,即尾符和阶符
那么一个64位的浮点数,便可规定其尾数为51位,阶码为11位,剩余两位为尾符和阶符(这个规定会因CPU厂家、型号的不同而不同)。

1.2 C++语言的历史

最初, 新语言被称为“带类的C"(C with Classes),1983年,”带类的C"加人了虚函数、函
数和运算符重载、引用等概念后,正式定名为"C++"(C Plus Plus)。
1989年.C++ 2.0版发布,加入了多重继承、抽象类、静态成员、常量成员函数等概念。
1990年稍后又加人了模板、异常处理、名字空间等机制。
1998年,ANSI和ISO标准委员会联合发布了至今最为广泛使用的C++标准,称为"C++98”. “C++98”的最重大改进就是加人了“标准模板库"(Standard Template Library,STL),使得
“泛型程序设计"成为C++除了“面向对象”外的另一主要特点。
2003 年,ISO的C++标准委员会又对C++略做了一些修订,发布了“C++03"标准。“C++03"和“C++98”的区别对大多数程序员来说可以不必关心。
2005 年,一份名为“Library Technical Report 1”(简称TR1)的技术报告发布,加人了正则表达式、哈希表等重要类模板。虽然TR1当时没有正式成为C++标准,但如今的许多C++编译器都已经支持TR1中的特性。
2011 年9月,ISO标准委员会通过了新的C++标准,这就是“C++ 11”。

下面有两个C++的网站,用来查询C++的各种函数、模板、类的用法特别方便。
(1) http://www. cplusplus. com/ ;
(2) http:// www. cppreference. com.

第二章 基本要素

2.1 标识符:

由大小写字母、数字、下划线构成。
中间不能有空格,长度不限,不能数字开头,大小写严格(N和n不是同一个变量名)。
【注意】:许多编译器自己内部用到的标识符是以双下划线开头的,命名时最好尽量避开双下划线开头。

2.2 关键字

C++语言预留了一些单词,这些单词具有特定的含义,不能被程序员用来作为标识符。
这些预留的单词就称为“关键字”,也称“保留字”。
C++语言中的常见的关键字如下(不同编译器可能会增加一些不同的关键字):
在这里插入图片描述
以上是C语言中都有的关键学,以下是C++中独有的关键字,
在这里插入图片描述
【注意】:index、list、link这几个会被某些C++编译器预留,所以最好避免。
【常用错】:index、do

2.3 常变量 、符号常量

(1)常变量关键字
语法举例:const int a = 5;
常变量的值只能用初始化的方式给出,此后不能再修改(否则会报编译错)。
(2)符号常量 : # define 常量名 常量值
语法举例:

  1. #define MAX_NUM 100
  2. #define NAME “Peeking University”

定义之后,程序中所有出现的MAX_NUM都是指 100,而NAME 都是指 “Peeking University” (包括双引号).
注意】:C语言最好多使用符号常量,少使用数值常量(方便后续修改),但是C++连符号常量都最好少用,尽可能使用常变量。

2.4 数据类型转换

(1)自动数据类型转换
自动类型转换不会改变赋值符号右边的变量。

2.5 运算符和表达式

(1)连等号赋值 : 计算顺序是从右到左
int a , b;

a = b = 5; // 先将 b 赋值为 5 ,然后求得b = 5 这个表达式的值为 5 ,再将这个 5 赋给 a;所以执行后 a和b 都 == 5.

(a = b) = 100; // 此时先计算 a = b,再将 a = b 的返回值 a 进行执行 = 100. 所以执行后 a == 100。
(2)优化运算速度
① 复合算术赋值运算符 会比 单纯的加减乘除运算符快很多。
eg. a += b;会比 a = a + b;快。
② 左/右移操作 会比 乘/除 快得多。
eg. 9 << 4; 会比 9 = 9 * (2 ^ 4);
左右移的时候不改变左操作数的值,而左移时高位丢弃,低位补0,右移的时候低位丢弃,高位会根据原先的符号位补上相同的符号位。
(3)有多项表达式时,赋最后一项
C++ 语言允许用逗号连接几个表达式,构成一个更大的表达式。而各个表达式的计算顺序是从左到右,最终会把第 n 个表达式赋给左操作数。
eg. int a = (3 + 4, 5 * 7, cnt ++ , 8 / 2); // a的值是4,即 8 / 2 的结果
(4)sizeof 运算符:求占用内存的字节数
既可以求变量,也可以求数据类型的字节数。
eg. // short 范围是[ - 2 ^ 15 , 2 ^ 15 - 1] , 一个字节有8位。
’short s = 5;
length1 = sizeof( s ); // length1 == 2
length2 = sizeof( short ); // length2 == 2
(5)在不考虑执行顺序时, ++ i 会比 i ++ 快。( - - 同理
(关乎运算符重载和标准模板库STL)

第三章 控制结构

3.1 switch语句:

char ch;
cin>> ch;
switch(ch) {
	case 'A' :
		cout << "Hello" << endl;
		//假如ch == 'A',但是这里没有break;语句的话,会接着执行case 'B'的语句,
		//直到遇到break;或者整个语句块执行结束。
		break;  
	case 'B' :
		cout << " World" << endl;
		break;
	default :
		cout << "Hello World !" <<endl;
}

3.2 goto语句

句子标号(即下面例子的Done)的命名规则和变量相同。

//goto在跳出多层循环时很方便,但其他使用情况一般会使得程序可读性变差,
//不容易搞清楚程序走向,所以尽可能避免使用。

# include <iostream>
using namespace std;
int main(){
	int n , m;
	cin >> n >> m;
	int i , j;
	for (i = n; i < m; ++ i) {
		for (j = i + 1; j <= m; ++ j) {
			if (i * j % (i + j) == 0) {
				goto Done;  //直接跳出所有循环,跳转去执行Done后面的语句
			}
		}
	}    // int a = max(x,y);
Done:
	if (i == m)  cout << "No solution.";
	else cout << i << "," << j ;
	return 0;
}

3.3 使用freopen方便调试

freopen 语句(实际上是调用了库函数)使得程序每一次执行时,都直接从文件中读取输入数据,这称为输入的重定向
【作用】:对于反复修改调试时,避免了从键盘输入同样的东西,尤其是在内容较多时。

# include <iostream>
using namespace std;
int main(){
	freopen("D:\\a-日常文档使用\\输入数据.txt", "r", stdin);  //字符串中的“\”要写两次才能真的代表“\”
	int n;
	cin>>n;
	while (n --) {
		int a, b;
		cin>> a>>b;
		cout<< (a > b ? a : b) << endl;
	}
	return 0;
}
/*
假如txt文本里面是:
4
2 5
4 3
12 90
8 7
那么程序将输出:
5
4
90
8
*/

第四章 函数

4.1 声明和定义

【声明】:
返回值类型 函数名 (参数类型1 参数名称1,参数类型2 参数名称2,……);
【 定义】: 必须在调用语句之前,否则需要写函数声明
返回值类型 函数名 (参数类型1 参数名称1,参数类型2 参数名称2,……){
语句;
}

4.2 main函数

同样可以有多个return语句。
return 0;表示程序正常执行结束,
而返回非0值则表示发生了一些异常或代表别的含义。

4.3 函数参数的默认值

//类型一: 默认常数参数
void Function1(int x = 20);
Function1();   //正确,此时默认传入的参数是20
Function1(8);  //正确,20只是默认参数值,依旧可以传入别的数据


//类型二: 用带有定义的表达式作为参数默认值
int Max(int m, int n);
int a, b;
void Function2(int x, int y = Max(a,b), int z = a * b){
}
Function2(4);      //正确,等效于Function2(4,Max(a,b),a*b);
Function2(4,9);    //正确,等效于Function2(4,9,a*b);
Function2(4,2,3);  //正确,等效于Function2(4,2,3);
Function2(4, ,3);  //错误!!!被省略的只能是最右边的几个

函数参数的默认值,可以像上面的Function1那样编写在声明函数的地方,也可以像Function2那样编写在定义函数的地方, 但是不能在两个地方都编写

一般来说,会以最常用的参数值作为函数参数的默认值。例如,有些整型参数在调用时经常给的实参是0,那么,不妨用0作为参数的默认值,这样编写函数调用语句时可以少输入参数,尤其在函数参数个数多时能省点事。

但这不是函数默认参数所带来的真正好处。真正的好处是使程序的可扩充性变好,即程序需要增加新功能时改动尽可能少

试想下面这种情况: 一个快编写好的绘图程序,程序中有个画圆的函数Circle,画出来的圆都是黑色的。突然觉得应该增加画彩色圆的功能,于是Circle函数就需要加个int类型的color参数,用来表示颜色。但是原来的程序中可能大多数调用Circle的地方依然是画个黑色的圆就可以了,只有少数几个地方需要改成画彩色的圆。此时,如果要找出所有调用Circle函数的语句并都补上颜色实参,那一定让人觉得很烦。而有了函数参数默认值的机制,则只需为Circle函数的新参数指定默认值0(假定0代表黑色),然后找出少数几个调用Circle画彩色圆的地方,补上颜色参数即可。实践中这种情况是经常有的。

4.4 引用

(1) 定义: 类型名 & 引用名 = 同类型的某变量名 ;

//定义的例子:
int n;
int & r = n;
int & q = r;
//在函数的例子
int n = 4;
int & SetValue() {
	return n;
}
int main() {
	SetValue() = 40;
	cout<< n << endl;     //输出40
	int & r = SetValue();
	cout<< r << endl;     //输出40
	return 0;
}

(2) 常引用

【tips】:
r 的类型就是 int & , 可以说 r 就是 n 的别名,修改 r 的值,n 也会跟着改变。
定义引用时,一定要初始化(通常用同类型变量初始化,但也可以用引用初始化引用,例如上面的q。总之,引用只能引用变量),否则编译无法通过。
初始化后,r 就不能引用别的变量了。
(2) 常引用
eg.
int n;
const int & s = n;
【和普通引用的区别】:
常引用不能修改引用内容,但是引用内容可以通过别的方式修改。(比如修改n,从而间接修改了 s 引用的内容)
T & 或者 T 可以初始化 const T &,但是反之不可。

4.5 内联函数

使用函数能够避免将相同代码重写多次的麻烦,还能减少可执行程序的体积,但也会带来程序运行时间上的开销。

函数调用在执行的时候首先要在栈上面为形参和局部变量分配存储空间,然后还要将实参的值复制给形参,接着还要将函数的返回地址(该地址指明了函数执行结束后程序应该回到哪里去继续执行)放人栈中,最后才跳转到丽数内部去执行,这些过程是要耗费时间的。另外,函数执行return语句返回时,需要从栈中回收形参和局部变量占用的存储空间,然后从栈中取出返回地址,再跳转到该地址去继续执行,这个过程也要耗费时间。

总之,使用函数调用语句和直接把函数里面的代码重新编写一遍方式相比,节省了人力,但是带来了程序运行时间上的额外开销。

一般情况下这个开销都可以忽略不计。但是如果一个函数内部本来就没有几条语句,执行时间本来就非常短,那么这个函数调用产生的额外开销和函数本身执行的时间相比,就显得不能忽略了。假如这样的函数在一个循环中被上千万次地执行,函数调用导致的时间开销就会使得程序明显变慢。作为特别注重程序执行效率,因而适合编写底层系统软件的高级程序设计语言,C++用 “ inline ” 关键字较好地解决了函数调用开销的问题。

inline int Max(int a, int b) {
	if ( a > b )  return a;
	return b;
}
int main() {
	int t;
	cin>>t;
	while (t --) {
		int a, b;
		cin >> a >> b;
		cout << Max(a, b) <<endl;
	}
	return 0;
}

加了 inline 关键字的函数即为" 内联函数 "。其与普通函数的区别在于,当编译器处理调用内联函数的语句时,不会把它编译成函数调用,而是将内联函数体内的代码整个插入到调用的位置,从而不需要付出执行函数调用的额外开销。

当然,使用内联函数会比一般的函数调用消耗空间(增加了可执行程序的体积),所以内联函数适用于函数体简单、执行次数少的情况。(本质上就是空间换时间)

【注意】:调用内联函数前,必须出现完整的内联函数的定义(即整个函数体),而不能只有声明。

4.6 库函数

4.6.1 数学函数 cmath

abs(x) : 求整型数X的绝对值
cos(x) : 求x(弧度)的余弦。
fabs(x) : 求浮点数x的绝对值。
ceil(x) : 求不小于x的最小整数。
floor(x) : 求不大于x的最大整数
log(x) : 求x的自然对数。
log10(x) : 求x的对数(底为10)。
pow(x,y) : 求x的y次方
sin(x) : 求x(弧度)的正弦。
sqrt(x) : 求x的平方根。

4.6.2 字符处理函数 ctype

int isdigit(int c) : 判断c是否是数字字符
int isalpha(int c) : 判断c是否是一个字母
int isalnum(int c) : 判断c是否是一个数字或字母。
int islower(int c) : 判断c是否是一个小写字母
int isupper(int c) : 判断c是否是一个大写字母
int toupper(int c) :如果c是小写字母,则返回对应的大写字母
int tolower(int c) : 如果c是大写字母,则返回对应的小写字母

第五章 数组

5.1 数组初始化

类似于:

int arr1[] = {1,2,3};       //合法,长度为3
int arr2[3] = {1,2,3};      //合法,等效于上一个数组
int arr3[5] = {1,2,3};      //合法,等效于{1,2,3,0,0}
int arr4[2][3] = {{2,3}};   //合法,等效于{{2,3,0},{0,0,0}}

5.2 数组大小限制

C++中数组不是可以任意开的,而且不同编译器要求不一。例如Dev C++所占字节数最多不能超过0x7f f f f f f f (即2 ^ 30 - 1, 1.073741823 * 10 ^ 9).

而假如数组是开在了函数内部,那么限制会更明显(因为函数内部的变量是在栈分配空间,而栈大小有限)。如果数组在函数内开太大,会很容易导致程序运行时因为栈溢出而崩溃

5.3 数组作为函数参数

【tips】:

  1. 数组作为函数参数时,是传引用的,所以形参数组改变了,实参数组也会改变。eg. sort (arr) ; 虽然只是将arr传给了sort函数,但是排序后,原先的arr也会改变(变为正序)。
  2. 二维数组作为形参时,必须写明数组有多少列,行数不做要求。

第六章 字符串

6.1 普通字符串(字符常量、字符数组)

char ch1[3] = {'1','0','3'};     //合法
for (int i = 0; i < sizeof(ch); i ++) 
	cout<<ch[i]<<" ";           //正常,会输出 1 0 3
	
ch1[1] = 0;
for (int i = 0; i < sizeof(ch); i ++) 
	cout<<ch[i]<<" ";           //不正常,会输出 1    3
cout << ch1 <<endl;             //不正常,只会输出 1 ,因为cout遇到 \0 就结束输出

ch1[1] = '0';
for (int i = 0; i < sizeof(ch); i ++) 
	cout<<ch[i]<<" ";           //正常,会输出 1 0 3

【tips】:

  1. C++的字符串(无论是常量、字符数组还是string),都会以 ’ \0 ’ (ASCII 码值为 0 ) 结尾。
  2. 字符串的长度不包括 ’ \0 ’ 。
  3. 字符串中包含双引号时,应在引号前面加上 ’ \ ',eg. "He said : " Hello ! " "; 等效于 " He said : " Hello ! "
  4. 字符常量 " " (不是string)也是合法字符,而且仍然会占据一个字节空间,因为存在 ’ \0 '。

6.2 string

6.2.1 string 对象的定义和赋值

定义时如果没有初始化,那么默认是空串 " ".

//定义string的方式
char ch[] = "123";
string str1;           //等效于""
string str2 = "abc";   
string str3 = str2;
string str4 = ch;      //可以直接用char数组赋值

与字符数组不同的是,string 对象的体积、大小是固定的(即占的内存是一定的,与存放的字符串长度无关),但是这个固定的值在不同编译器上会有所不同。
eg. Dev C++ 中 sizeof(string)是 4 ,而Visual studio 2010 是32.

所以其实,string对象在互相赋值时,其实是不需要考虑两者等不等长的问题

由此也可发现, string对象并不会直接存放字符串,而是在别处开辟内存空间存放字符串,而string对象只是存放该空间的地址或者再加上一些其他的信息。

6.2.2 string对象的运算

  1. string对象之间可以用 < 、 <= 、 == 、 >= 、 > 等运算符进行比较。
    比较时,按词典顺序,从左到右,且大小写相关。但是由于大写字母的ASCII码值小于小写字母,所以 " Zbc " < " abc "。

  2. += 同样适用于string,用法同数值的+=。

6.3 常用方法

6.3.1 输入

cin 读入字符串时,遇到空格和换行都会停止读入,而用getline(char buf [ ] , int bufSize)函数可以从键盘读入一整行(但是读入的字符个数最多只有bufSize - 1 个, 哪怕一行不止这么多个 ➡ 避免buf溢出)到内存缓冲区 buf 中,并且在末尾自动添加 \0 。

6.3.2 cstring头文件里的方法

除stricmp外,这些函数都是大小写相关的。

  1. char *strcat(char *dest, const char * src);
    将字符串src连接到dest后面。执行后src不变,dest变长了。返回值是dest.

  2. char *strchr(const char *str, int c);
    寻找字符C在字符串str中第一次出现的位置。如果找到,就返回指向该位置的 char * 指针;
    如果str中不包含字符c,则返回NULL.

  3. char *strrchr(const char str,char c);
    寻找字符c在字符串str中最后一次出现的位置。如果找到,就返回指向该位置的char
    指针;如果str中不包含字符c,则返回NULL.

  4. char *strstr(const char *str, const char *subStr);
    寻找子串subStr在str中第一次出现的位置。 如果找到,就返回指向该位置的指针;
    如果str中不包含字符串subStr,则返回NULL.

  5. int strcmp(const char *s1,const char *s2);
    字符串比较。如果s1小于s2,则返回负数;如果s1等于s2,则返回0;s1大于s2,则返回正数。

  6. int stricmp(const char *s1,const char *s2);
    大小写无关的字符串比较。如果sl小于s2,则返回负数;如果sl等于s2,则返回0; s1大于s2,则返回正数。不同的编译器实现此函数的方法有所不同,有的编译器是将s1、s2都转换成大写字母后再比较;有的编译器是将s1、s2都转换成小写字母再比较。这样,在s1或s2中包含ASCII 码介于‘Z’和‘a’之间的字符时(即 ‘ [ ’、‘ \ ’、‘’ ] 、’ ^ ’、‘ _ ’、‘ ` ’ 这6
    个字符)不同编译器编译出来的程序,执行stricmp的结果就可能不同。

  7. char *strcpy(char *dest, const char *src);
    将字符串src复制到dest。返回值是dest。

  8. int strlen(const char *s);
    求字符串s的长度,不包括结尾的 ’ \0 '.
    (或者:int size(char *s)😉

  9. char *strlwr(char *str);
    将str中的字母都转换成小写。返回值就是str.

  10. char *strupr(char *str);
    将str中的字母都转换成大写。返回值就是str。

  11. char *strncat(char * dest, const char *src, int n);
    将src的前n个字符连接到dest尾部。如果src长度不足n.则连接src全部内容。返回值是 dest.

  12. int strncmp(const char *s1, const char *s2, int n);
    比较s1前n个字符组成的子串和s2前n个字符组成的子串的大小。
    若长度不足n,则取整个串作为子串。返回值和strcmp类似。

  13. char *strncpy(char *dest, const char *src, int n);
    复制src的前n个字符到dest.如果src长度大于或等于n,
    ,该函数不会自动往dest中写入‘ \0 '; 若src长度不足n.则复制src的全部内容以及结尾的^\0’到dest,返回值是dest。

  14. char * strtok(char *str, const char *delim);
    连续调用该函数若干次,可以做到:从str中逐个抽取出被字符串delim 中的字符分隔开的若干个子串。

  15. int atoi(char *s);
    将字符串s中的内容转换成一个整型数返回。例如,如果字符串s的内容是“1234”,那么函数返回值就是1234。如果s格式不是一个整数,如是"a12" ,那么返回0。

  16. double atof(char *s);
    将字符串s中的内容转换成实数返回。例如,"12. 34"就会转换成12. 34。如果S的格式不是一个实数,则返回0。

  17. char *itoa(int value, char *string, int radix);
    将整型值 value 以 radix 进制表示法写入string。例如 :
    char szValue[ 20];
    itoa(27, szValue, 10); //使得szValue的内容变为"27"
    itoa(27, szValue, 16); //使得szValue的内容变为"1b”

第七章 指针

7.1 基本概念

(1)定义:
类型名 * 指针变量名;
eg. int *p = 10000;

其中,p 是一个指针,类型为 int *,代表地址10000;*p 是一个int型的变量,而该变量可以读写地址10000开始的sizeof(T)个字节的内容。

通俗来讲, p 是地址,* p是变量,而 “ * ” 则称为间接引用运算符。

【注意】:

  1. 无论 T 是什么类型, sizeof(T *)的值都是 4,即 4 个字节。
  2. 在实际编程中,极少需要像前面的“int * p = (int * ) 40000;”那样,直接给指针赋一个常数地址值。直接读写某个常数地址处的内容,常常会导致程序出错,因为像40000这个地址中存放的是什么,谁也不知道,往40000这个地址中写数据,也许会造成一些数据破坏。
  3. 指针的通常用法是: 将一个T类型的变量x的地址,赋值给一个类型为“T * ” 的指针p(俗称“让p指向x”), 此后表达式“* p”代表p所指向的变量,即x,通过 “* p” 就能读取或修改变量x的值。(往往借助 & 去取 x 的地址,或者用 x 的引用
  4. 在作为函数形参时,T *p 和 T p[ ] 是完全等价的。(数组名字实际上就是一个指针,指向数组的起始位置。)

7.2 指针的作用

其实,如果需要修改一个变量的值,直接使用该变量就可以了,不需要通过指向该变量的指针来进行。那么指针到底有什么特殊的用途呢?

的确,并不是所有的程序设计语言都有“指针”的概念,如Basic、Java语言都没有。但是“指针”在 C++中是十分重要的概念,有了指针,就有了可以自由地访问内存空间的手段,用C++编写程序可以更加灵活、高效。

C+是一种强调程序执行效率的语言,更贴近硬件底层,因而能够胜任如操作系统、设备驱动程序、3D游戏等对运行效率要求很高的软件开发。指针提供了不需要通过变量,就能对内存直接进行操作的手段。

通过指针程序能访问的内存区域就不再限于变量所占据的数据区域了,存放程序指令的指令区,别的程序的数据区、指令区,甚至操作系统的数据区和指令区,都可能被程序访问和修改,这也是病毒、木马程序和反病毒软件能够工作的关键。《新标准C++程序设计教程》一书的作者曾经编写过一个Windows平台上的能够鼠标取词并翻译的词典软件,这种软件在运行期间都需要往其他程序的内存空间中注入自己的代码,并且修改其他程序在内存中的指令,这样才能拦截其他程序在屏幕上输出的文字,从而实现鼠标移动到其他程序的文字上就能弹出翻译框的功能。这样的程序,使用没有指针的Basic或Java语言,是难以实现的,因为没有访问别的程序的内存空间的手段。

举一个容易理解的例子.如果定义了变量"int a”,在没有指针的程序设计语言中,程序只能访问a占据的内存区域:不能访问a前面和后面的内存区域。而在C++语言中,只要用个指针指向 a 的地址,然后对p进行加减操作,P就能够指向a后面或前面的内存区域,通过P也就能访问这些内存区域了。

需要注意的是,指针的灵话性带来的副作用就是使用指针的程序更容易出错,所以使用指针要慎重,不要滥用

7.3 指针运算

  1. 同类型指针比较大小
    其实就是比较地址大小

  2. 同类型指针相减
    x - y = (x地址 - y地址) / sizeof(T)

  3. 指针 p 加或减 一个整型变量或常量 n
    p + n = 地址p + n × sizeof(T)
    (减法同理)
    (最终得到的还是一个地址)
    (注意不是n,而是n * 字节数)

  4. 指针p 自增自减:p的地址加减上sizeof(T)
    (感觉上是取 *p 相邻的值)

  5. 用下标运算符 [ ] 运算
    p : 指针
    n :整型变量或常量
    p [ n ] 等价于 *( p + n ) // 数组访问值的原理?
    (感觉上是实现了地址的自定义加减任意数?)

7.4 空指针

在C++语言中,可以用“NULL”关键字对任何类型的指针进行赋值。值为NULL的指针,被称做空指针。空指针指向地址0,因为NULL实际上就是整数0。例如: int *pn = NULL; char *pc = NULL; int *p2 = 0;

pn、pc、p2中存放的地址都是0。程序不能够在地址0处进行读写

指针也可以作为条件表达式使用。如果指针的值为NULL,则相当于为假,值不为NULL,就相当于为真。假设p是一个指针,则“if( p == NULL )”和“if( !p )”是等价的,“if(p != NULL )”和“if§”也是等价的。

在C++ 11的标准中,增加了nullptr 关键字,用于表示空指针。nullptr虽然内容是0,但不能把它看做是int类型的。用NULL给int类型的变量赋值可以,但是用nullptr给int类型变量赋值是不可以的

7.5 常量指针

//第一种定义方式:  const T *p;
//表示 p 指向的是一个T型的常量,所以不能通过这样的常量指针去修改指向的内容(但可以通过其他这类型的常量指针以外的变量去修改)
 
//举例:
const int *p;
int n = 100;
p = &n;
//*p = 200;   //编译报错,不能通过常量指针去修改
n = 300;      //正确(可以修改不是常量指针的变量)
//第二种定义方式:  T *const p; 
//表示 p 这个指针本身是常量,所以这样定义的常量指针,只能在初始化的时候让它指向某处,
//此后,它再也不能指向别处。但是!!可以通过它去修改它指向的内容。
//第三种定义方式: const T *const p = val;
//这样定义常量指针,意味着p本身是个常量,不能更改指向的地址,同时它指向的内容也是常量,不能修改内容,
//那么这样子其实 p 就相当于是一个普普通通老老实实的常量const T了,所以一般这种写法适用性不高

所以,其实在哪个位置加上const就是定义哪里为常量,可以根据需求调整。

7.6 void 指针和内存操作库函数memset和mencpy

void指针,依个人浅见,可以看成是不限类型的指针,或者说是一个泛型指针,因为可以用任何类型的指针对void指针进行赋值和初始化,有别于空指针指向地址0

但是因为void代表了任意类型,所以sizeof ( void ) 是没有定义的,故而对 void * 类型的指针,不能够进行前面提到过的指针运算

void指针主要用于内存复制:将内存中的某一块内容复制到另一块中,那么源块和目的块的地址都可以用void指针表示。

C++中有两个常用的内存操作的标准库函数:memset 和 mencpy

  1. void *memset (void *dest, int ch, int n);
    作用: 将从dest开始的n个字节都设置成ch。
    eg. memset (arr, false, sizeof(arr)) ;
  2. void *memcpy ( void *dest, void *src, int n);
    作用:将地址src开始的n个字节,复制到地址dest。返回值是dest。

7.7 函数指针

7.8 指针和动态内存分配

7.9 指向指针的指针

第八章 自定义数据类型

8.1 结构的定义与使用

//定义
struct 结构名 {
	类型名  成员变量名1;  //eg. int age;
	类型名  成员变量名2;
	……
};

//定义举例:
struct Student {
	string name;
	int age;
	float fGPA;
};

struct Teacher {
	string name;
	int age;
	float money;
}tec1, tec2;       //此时便定义了两个Teacher变量 tec1 和 tec 2

Student  stu1, stu2;  //或者这样定义结构变量。

//访问成员变量
stu1.age = 18;
int *p = &stu1.age;   //p指向了stu1中的age成员变量

//初始化
Student stu3 = {"Tom", 19, 3.8};

//指向结构变量的指针
Student *p = &stu3;
cout<< p -> name;  //输出“Tom”
cout<< (*p).age;   //输出“19”

【注意】:

  1. 像Student和Teacher这样统称为结构类型,tec1、tec2、stu1、stu2则统称为结构变量,而其成员变量(name、age等)则称为结构的域
  2. 结构定义其实也是在定义一种数据类型的变量,所以 {} 后需要加上分号,就像定义“ int a;”在末尾加上;表示定义完毕。
  3. 相同类型的结构变量可以相互赋值,但是同类型结构变量之间不能用==、!=、> 等比较符号进行运算
  4. 结构变量所占内存大小是所以成员变量内存大小之和,且一般同一个结构变量,成员变量在内存中是连续存放的,定义在前,地址也在前。
  5. 结构的成员变量可以是任何类型,包括基本数据类型、其他结构类型、自己本身的结构类型(例如链表的next指针)等。
  6. 指向结构变量的指针:
    (1)定义: 结构名 *指针变量名;
    (2)访问成员变量:
    ①指针 -> 成员变量名
    ②( *指针).成员变量名
  7. 结构变量、结构数组都是可以动态分配存储空间的。
  8. 结构变量本身和它的引用都可以直接作为函数参数,但不同的是:对于void dfs (Student stu); ,参数stu会复制传进来的Student变量,假如该结构体体积较大,那么这个复制操作会花费不必要的时间,尤其是递归调用或者需要多次调用该函数时。
    所以我们可以考虑使用“ 引用 ” 作为函数参数,此时参数传递的只是4个字节的地址,大大减少了时间和空间的开销,如 void dfs (const Student & stu) ;
    此处stu可以访问传进来的Student变量的所有信息,const 是为了确保函数中不会修改stu的值,以及告诉程序员,该函数不该修改它。

8.2 联合

联合的定义形式看起来和结构差不多,但是联合的的成员变量都是从相同的地址开始存放的,成员变量的储存空间是重叠的,故而整个联合变量的体积等于体积最大的成员变量的体积

//定义:
 union  联合名 {
	类型名  成员变量名1;  //eg. int age;
	类型名  成员变量名2;
	……
};

//用联合模拟寄存器
#include <iostream>
using namespace std;
union Register {
	unsigned int word; // word此处是“字”的意思,表示32位
	short LL;
	struct{
		unsigned short L;   //寄存器的低16位
		unsigned short H;   //寄存器的高16位
	}data;
};
int main(){
	Register AX;
	AX.word = 0x12345678;
	AX.data.H = 0x9999;
	// hex告诉cout,此后的整数均以十六进制输出
	cout<< hex<< AX.word<< ","<< AX.LL<< ","<< AX.data.L<< ","<< AX.data.H;    //输出:99995678,5678,5678,9999
	return 0;
}

在用联合模拟寄存器时, Register变量的各个成员变量内存储存位置如下:
在这里插入图片描述
换而言之,在Register变量中,word是int型,所以占了32位。
而LL由于其本身的数据类型只有16位,所以LL其实相当于word的低16位(注意!这里是十六进制的数值,储存起始点在右边)。
而在Register内还开了一个结构,因为struct的成员变量是连续存放的,所以据上图也可知,data . H的地址起始点不是从低位开始,而是接在data . L后面。

联合的应用之一:

C++是比较接近硬件底层的语言,甚至可以内嵌汇编语言,进行底层的软件开发。此时
联合就比较有用。例如,32 位的英特尔或其兼容CPU中有若干个32位的寄存器,其中4个通用寄存器在汇编语言编程时称为AX,BX,CX,DX。做算术运算等各种运算时,都需要先将变量从内存复制到寄存器,然后才能计算。计算结果也是先放在寄存器中,然后再复制到内存。编写汇编语言指令时,可以访问这4个通用寄存器的全部内容,也可以只访问这4个通用寄存器的高16位或低16位。AX寄存器的高16位,在汇编语言中被称为AH,AX寄存器的低16位,在汇编语言中被称为AL.同理,还有BH、BL、CH、CL等。

用C+编程时,如果想设置一种32位的变量和通用寄存器相对应,既能方便地访问整个32位的内容,又能方便地单独访问高16位或低16位,就可以使用联合来完成此任务。

在进行网络编程时,常会用到IP地址
IP地址是由4段组成的,如“192. 168. 12. 13”。每段的最大值是255,可以用一个字节表示,因此用一个unsigned int 变量就能保存IP地址,变量的每个字节正好对应于一段。用一个联合变量和IP地址对应,就既可以方便地访问整个IP地址,也能方便地只访问任何一段。

故而,就个人观感,联合适用于对于某一个数据,它某部分的字节有特殊的用途,那么就利用内存空间重叠的特点来最大限度便利我们的抽取操作

8.3 枚举类型

//定义:
enum 枚举类型名 {枚举值1,枚举值2,……,枚举值n};

//用法:
enum Weekday{Mon, Tue, Wed, Thu, Fri, Sat, Sun};
Weekday workingDay = Sun;
//枚举值和枚举类型变量可以被自动转换成整型值。
//对应的规则是:枚举值1 = 0, 枚举值2 = 1, 枚举值3 = 2 ……
cout<< Mon << "," << Wed << "," << workingDay << endl;  //输出:0,2,6。
workingDay = 3; //则会报错,因为workingDay没有“3”这个枚举值,只有Mon~Sun。

//假设指定了枚举值i的值为n,那么如果枚举值i + 1没有被指定,则其值为n + 1
//red:7  white:8  black:9  blue:6  green:7  yellow:8
enum CarColor {red = 7, white, black, blue = 6, green, yellow};  


8.4 用typedef定义类型

使用typedef关键字可以给类型的名字起一个别名,用于简化书写。
比如说 typedef long long ll; 将long long 定义成 ll。

第九章 程序设计的基本思想

第十章 C++程序结构

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值