1.变量的作用域
1)全局变量
在整个程序生命周期内都是有效的,在定义位置之后的任意函数中都能访问。
全局变量在主程序退出时由系统收回内存空间。
2)局部变量
在函数或语句块内部的语句使用,在函数或语句块外部是不可用的。
局部变量在函数返回或语句块结束时由系统收回内存空间。
3)静态局部变量
用static修饰的局部变量生命周期和程序相同,并且只会被初始化一次。
其作用域为局部,当定义它的函数或语句块结束时,其作用域随之结束。
当程序想要使用全局变量的时候应该先考虑使用static(考虑到数据安全性)。
4)注意事项
- 全局变量和静态局部变量自动初始化为0。
- 局部变量不会自动初始化,其值是不确定的,程序中应该有初始化局部变量的代码,否则编译可能会报错(不同的编译器不一样)。
- 局部变量和全局变量的名称可以相同,在某函数或语句块内部,如果局部变量名与全局变量名相同,就会屏蔽全局变量而使用局部变量,如果想使用全局变量,可以在变量名前加两个冒号(::)。
- for循环初始化语句中定义的变量的作用域是for语句块。
2.函数分文件编写
头文件(*.h):需要包含的头文件,声明全局变量,函数的声明,数据结构和类的声明等。
源文件(*.cpp):函数的定义、类的定义。
3.字符串
C++中的字符串操作可以分为两种主要风格:
C风格字符串和C++风格字符串。以下是它们的特点和常用操作,并附带相应的示例代码:
C风格字符串:
- C风格字符串的本质是字符数组。
- 字符数组以null终止,即以
\0
表示字符串的结束。 - 在C风格字符串中,你需要手动分配内存,并负责管理字符串的长度和内存。
C++风格字符串:
- C++风格字符串的本质是
std::string
类,它封装了字符串操作。 std::string
自动管理内存,无需手动分配或释放内存。- 支持丰富的字符串操作,提供更高的安全性和便捷性。
C++风格字符串的常用操作:
- 赋值:
std::string str = "字符串内容";
- 拼接:
str = str + "更多字符串内容";
- 追加:
str += "追加的字符串";
- 长度:
int length = str.length();
- 比较:
if (str1 == str2) { /* 相等 */ } else { /* 不相等 */ }
- 截取子串:
std::string subStr = str.substr(startIndex, length);
- 查找子串:
size_t found = str.find("目标子串");
#include <iostream>
#include <string>int main() {// 赋值操作std::string str1 = "Hello, ";std::string str2 = "world!";// 拼接操作std::string result = str1 + str2;std::cout << "拼接后的字符串: " << result << std::endl;// 追加操作result += " Welcome to C++!";std::cout << "追加后的字符串: " << result << std::endl;// 字符串长度std::cout << "字符串长度: " << result.length() << std::endl;// 字符串比较std::string compareStr1 = "Hello, world!";std::string compareStr2 = "Welcome to C++!";if (compareStr1 == compareStr2) {std::cout << "字符串相等" << std::endl;} else {std::cout << "字符串不相等" << std::endl;}// 截取子串std::string subStr = result.substr(7, 5); // 从位置7开始截取5个字符std::cout << "截取的子串: " << subStr << std::endl;// 查找子串size_t found = result.find("C++"); // 查找C++在字符串中的位置if (found != std::string::npos) {std::cout << "子串 \"C++\" 找到,位置:" << found << std::endl;} else {std::cout << "子串 \"C++\" 未找到" << std::endl;}return 0;
}
C风格
C语言约定:如果字符型(char)数组的末尾包含了空字符\0(也就是0),那么该数组中的内容就是一个字符串。
memset(name,0,sizeof(name)); // 把全部的元素置为0。
name[0]=0; // 不规范,有隐患,不推荐。char *strcpy(char* dest, const char* src);
功 能: 将参数src字符串拷贝至参数dest所指的地址。
返回值: 返回参数dest的字符串起始地址。
复制完字符串后,会在dest后追加0。
如果参数dest所指的内存空间不够大,会导致数组的越界。char * strncpy(char* dest,const char* src, const size_t n);
功能:把src前n个字符的内容复制到dest中。
返回值:dest字符串起始地址。
如果src字符串长度小于n,则拷贝完字符串后,在dest后追加0,直到n个。
如果src的长度大于等于n,就截取src的前n个字符,不会在dest后追加0。
如果参数dest所指的内存空间不够大,会导致数组的越界。size_t strlen( const char* str);
功能:计算字符串的有效长度,不包含0。
返回值:返回字符串的字符数。
strlen()函数计算的是字符串的实际长度,遇到0结束。字符串拼接strcat()
char *strcat(char* dest,const char* src);
功能:将src字符串拼接到dest所指的字符串尾部。
返回值:返回dest字符串起始地址。
dest最后原有的结尾字符0会被覆盖掉,并在连接后的字符串的尾部再增加一个0。
如果参数dest所指的内存空间不够大,会导致数组的越界。字符串比较strcmp()和strncmp()
int strcmp(const char *str1, const char *str2 );
功能:比较str1和str2的大小。
返回值:相等返回0,str1大于str2返回1,str1小于str2返回-1;
int strncmp(const char *str1,const char *str2 ,const size_t n);
功能:比较str1和str2前n个字符的大小。
返回值:相等返回0,str1大于str2返回1,str1小于str2返回-1;
两个字符串比较的方法是比较字符的ASCII码的大小,从两个字符串的第一个字符开始,如果分不出大小,就比较第二个字符,如果全部的字符都分不出大小,就返回0,表示两个字符串相等。
在实际开发中,程序员一般只关心字符串是否相等,不关心哪个字符串更大或更小。查找字符strchr()和strrchr()
const char *strchr(const char *s, int c);
返回在字符串s中第一次出现c的位置,如果找不到,返回0。
const char *strrchr(const char *s, int c);
返回在字符串s中最后一次出现c的位置,如果找不到,返回0。查找字符串strstr()
char *strstr(const char* str,const char* substr);
功能:检索子串在字符串中首次出现的位置。
返回值:返回字符串str中第一次出现子串substr的地址;如果没有检索到子串,则返回0。
4.指针
1)指针变量
指针变量简称指针,它是一种特殊的变量,专用于存放变量在内存中的起始地址。
语法:数据类型 *变量名;
数据类型必须是合法的C++数据类型(int、char、double或其它自定义的数据类型)。
星号*与乘法中使用的星号是相同的,但是,在这个场景中,星号用于表示这个变量是指针。
2)对指针赋值
不管是整型、浮点型、字符型,还是其它的数据类型的变量,它的地址都是一个十六进制数。我们用整型指针存放整数型变量的地址;用字符型指针存放字符型变量的地址;用浮点型指针存放浮点型变量的地址,用自定义数据类型指针存放自定义数据类型变量的地址。
语法:指针=&变量名;
注意
- 对指针的赋值操作也通俗的被称为“指向某变量”,被指向的变量的数据类型称为“基类型”。
- 如果指针的数据类型与基类型不符,编译会出现警告。但是,可以强制转换它们的类型。
3)指针占用的内存
指针也是变量,是变量就要占用内存空间。
在64位的操作系统中,不管是什么类型的指针,占用的内存都是8字节。
在C++中,指针是复合数据类型,复合数据类型是指基于其它类型而定义的数据类型,在程序中,int是整型类型,int*是整型指针类型,int*可以用于声明变量,可以用于sizeof运算符,可以用于数据类型的强制转换,总的来说,把int*当成一种数据类型就是了。
4)使用指针
声明指针变量后,在没有赋值之前,里面是乱七八糟的值,这时候不能使用指针。
指针存放变量的地址,因此,指针名表示的是地址(就像变量名可以表示变量的值一样)
*运算符被称为间接值或解除引用(解引用)运算符,将它用于指针,可以得到该地址的内存中存储的值,*也是乘法符号,C++根据上下文来确定所指的是乘法还是解引用。
5)指针用于函数的参数
如果把函数的形参声明为指针,调用的时候把实参的地址传进去,形参中存放的是实参的地址,在函数中通过解引用的方法直接操作内存中的数据,可以修改实数的值,这种方法被通俗的称为地址传递或传地址。
值传递:函数的形参是普通变量。
传地址的意义如下:
- 可以在函数中修改实参的值。
- 减少内存拷贝,提升性能。
6)const 修饰指针
语法:const 数据类型 *变量名;(左定值)
不能通过解引用的方法修改内存地址中的值(用原始的变量名是可以修改的)。
注意:
- 指向的变量(对象)可以改变(之前是指向变量a的,后来可以改为指向变量b)。
- 一般用于修饰函数的形参,表示不希望在函数里修改内存地址中的值。
- 如果用于形参,虽然指向的对象可以改变,但这么做没有任何意义。
如果形参的值不需要改变,建议加上const修饰,程序可读性更好。
语法:数据类型 * const 变量名;
指向的变量(对象)不可改变。
注意:
- 在定义的同时必须初始化,否则没有意义。
- 可以通过解引用的方法修改内存地址中的值。
C++编译器把指针常量做了一些特别的处理,改头换面之后,有一个新的名字,叫引用。
7)空指针
如果对空指针解引用,程序会崩溃。
如果对空指针使用delete运算符,系统将忽略该操作,不会出现异常。所以,内存被释放后,也应该把指针指向空。
在函数中,应该有判断形参是否为空指针的代码,目的是保证程序的健壮性。
为什么空指针访问会出现异常?
NULL指针分配的分区:其范围是从 0x00000000到0x0000FFFF。这段空间是空闲的,对于空闲的空间而言,没有相应的物理存储器与之相对应,所以对这段空间来说,任何读写操作都是会引起异常的。空指针是程序无论在何时都没有物理存储器与之对应的地址。为了保障“无论何时”这个条件,需要人为划分一个空指针的区域,固有上面NULL指针分区。
8)野指针
野指针就是指针指向的不是一个有效(合法)的地址。
在程序中,如果访问野指针,可能会造成程序的崩溃。
出现野指针的情况主要有三种:
1)指针在定义的时候,如果没有进行初始化,它的值是不确定的(乱指一气)。
2)如果用指针指向了动态分配的内存,内存被释放后,指针不会置空,但是,指向的地址已失效。
3)指针指向的变量已超越变量的作用域(变量的内存空间已被系统回收),让指针指向了函数的局部变量,或者把函数的局部变量的地址作为返回值赋给了指针。
规避方法:
1)指针在定义的时候,如果没地方指,就初始化为nullptr。
2)动态分配的内存被释放后,将其置为nullptr。
3)函数不要返回局部变量的地址。
注意:野指针的危害比空指针要大很多,在程序中,如果访问野指针,可能会造成程序的崩溃。是可能,不是一定,程序的表现是不稳定,增加了调试程序的难度。
4.void关键字
在C++中,void表示为无类型,主要有三个用途:
1)函数的返回值用void,表示函数没有返回值。
void func(int a,int b)
{
// 函数体代码。
return;
}
2)函数的参数填void,表示函数不需要参数(或者让参数列表空着)。
int func(void)
{
// 函数体代码。
return 0;
}
3)的形参用void *,表示接受任意数据类型的指针。
注意:
- 不能用void声明变量,它不能代表一个真实的变量,但是,用void *可以。
- 不能对void *指针直接解引用(需要转换成其它类型的指针)。
- 把其它类型的指针赋值给void*指针不需要转换。
- 把void *指针赋值给把其它类型的指针需要转换。
5.动态内存分配
使用堆区的内存有四个步骤:
1)声明一个指针;
2)用new运算符向系统申请一块内存,让指针指向这块内存;
3)通过对指针解引用的方法,像使用变量一样使用这块内存;
4)如果这块内存不用了,用delete运算符释放它。
申请内存的语法:new 数据类型(初始值); // C++11支持{}
如果申请成功,返回一个地址;如果申请失败,返回一个空地址(暂时不考虑失败的情况)。
释放内存的语法:delete 地址;
释放内存不会失败(还钱不会失败)。
- 动态分配出来的内存没有变量名,只能通过指向它的指针来操作内存中的数据。
- 如果动态分配的内存不用了,必须用delete释放它,否则有可能用尽系统的内存。
- 动态分配的内存生命周期与程序相同,程序退出时,如果没有释放,系统将自动回收。
- 就算指针的作用域已失效,所指向的内存也不会释放。
- 用指针跟踪已分配的内存时,不能跟丢。
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。int main()
{// 1)声明一个指针;// 2)用new运算符向系统申请一块内存,让指针指向这块内存;// 3)通过对指针解引用的方法,像使用变量一样使用这块内存;// 4)如果这块内存不用了,用delete运算符释放它。// 申请内存的语法:new 数据类型(初始值); // C++11支持{}// 释放内存的语法:delete 地址;int* p = new int(5);cout << "*p=" << *p << endl;*p = 8;cout << "*p=" << *p << endl;delete p;/* for (int ii = 1; ii > 0; ii++){int* p = new int[100000]; // 一次申请100000个整数,这个语法以后再讲。cout << "ii="<<ii<<",p=" << p << endl;}/*
}
6.一维指针和数组
1)指针的算术
将一个整型变量加1后,其值将增加1。
但是,将指针变量(地址的值)加1后,增加的量等于它指向的数据类型的字节数。
2)数组的地址
a)数组在内存中占用的空间是连续的。
b)C++将数组名解释为数组第0个元素的地址。
c)数组第0个元素的地址和数组首地址的取值是相同的。
d)数组第n个元素的地址是:数组首地址+n
e)C++编译器把 数组名[下标] 解释为 *(数组首地址+下标)
3)数组的本质
数组是占用连续空间的一块内存,数组名被解释为数组第0个元素的地址。C++操作这块内存有两种方法:数组解释法和指针表示法,它们是等价的。
4)数组名不一定会被解释为地址
在多数情况下,C++将数组名解释为数组的第0个元素的地址,但是,将sizeof运算符用于数据名时,将返回整个数组占用内存空间的字节数。
可以修改指针的值,但数组名是常量,不可修改。
5)用new动态创建
普通数组在栈上分配内存,栈很小;如果需要存放更多的元素,必须在堆上分配内存。
动态创建一维数组的语法:数据类型 *指针=new 数据类型[数组长度];
释放一维数组的语法:delete [] 指针;
注意:
- 动态创建的数组没有数组名,不能用sizeof运算符。
- 可以用数组表示法和指针表示法两种方式使用动态创建的数组。
- 必须使用delete[]来释放动态数组的内存(不能只用delete)。
- 不要用delete[]来释放不是new[]分配的内存。
- 不要用delete[]释放同一个内存块两次(否则等同于操作野指针)。
- 对空指针用delete[]是安全的(释放内存后,应该把指针置空nullptr)。
- 声明普通数组的时候,数组长度可以用变量,相当于在栈上动态创建数组,并且不需要释放。
- 如果内存不足,调用new会产生异常,导致程序中止;如果在new关键字后面加(std::nothrow)选项,则返回nullptr,不会产生异常。
- 为什么用delete[]释放数组的时候,不需要指定数组的大小?因为系统会自动跟踪已分配数组的内存。
6)数组排序
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。int compasc(const void* p1, const void* p2) // 升序的回调函数。
{return *((int*)p1) - *((int*)p2);
}int compdesc(const void* p1, const void* p2) // 降序的回调函数。
{return *((int*)p2) - *((int*)p1);
}int main()
{int a[8] = { 4,2,7,5,8,6,1,3 };qsort(a,sizeof(a)/sizeof(int),sizeof(int),compasc); // 对数组a进行升序排序。for (int ii = 0; ii < 8; ii++){cout << "a[" << ii << "]=" << a[ii] << endl;}qsort(a, sizeof(a) / sizeof(int), sizeof(int), compdesc); // 对数组a进行降序排序。for (int ii = 0; ii < 8; ii++){cout << "a[" << ii << "]=" << a[ii] << endl;}
}
7.共同体
共同体(共用体、联合体)是一种数据格式,它能存储不同的数据类型,但是,在同一时间只能存储其中的一种类型。
声明共同体的语法:
union 共同体名
{
成员一的数据类型 成员名一;
成员二的数据类型 成员名二;
成员三的数据类型 成员名三;
......
成员n的数据类型 成员名n;
};
注意:
- 共同体占用内存的大小是它最大的成员占用内存的大小(内存对齐)。
- 全部的成员使用同一块内存。
- 共同体中的值为最后被赋值的那个成员的值。
- 匿名共同体没有名字,可以在定义的时候创建匿名共同体变量(VS和Linux有差别),也可以嵌入结构体中。
应用场景:
- 当数据项使用两种或更多种格式(但不会同时使用)时,可节省空间(嵌入式系统)。
- 用于回调函数的参数(相当于支持多种数据类型)。
8.引用
引用形参和const
如果引用的数据对象类型不匹配,当引用为const时,C++将创建临时变量,让引用指向临时变量。
什么时候将创建临时变量呢?
- 引用是const。
- 数据对象的类型是正确的,但不是左值。
- 数据对象的类型不正确,但可以转换为正确的类型。
结论:如果函数的实参不是左值或与const引用形参的类型不匹配,那么C++将创建正确类型的匿名变量,将实参的值传递给匿名变量,并让形参来引用该变量。
将引用形参声明为const的理由有三个:
- 使用const可以避免无意中修改数据的编程错误。
- 使用const使函数能够处理const和非const实参,否则将只能接受非const实参。
- 使用const,函数能正确生成并使用临时变量。
左值是可以被引用的数据对象,可以通过地址访问它们,例如:变量、数组元素、结构体成员、引用和解引用的指针。
非左值包括字面常量(用双引号包含的字符串除外)和包含多项的表达式。
引用用于函数的返回值
传统的函数返回机制与值传递类似。
函数的返回值被拷贝到一个临时位置(寄存器或栈),然后调用者程序再使用这个值。
double m=sqrt(36); // sqrt()是求平方根函数。
sqrt(36)的返回值6被拷贝到临时的位置,然后赋值给m。
cout << sqrt(25);
sqrt(25)的返回值5被拷贝到临时的位置,然后传递给cout。
如果返回的是一个结构体,将把整个结构体拷贝到临时的位置。
如果返回引用不会拷贝内存。
语法:
返回值的数据类型& 函数名(形参列表);
注意:
- 如果返回局部变量的引用,其本质是野指针,后果不可预知。
- 可以返回函数的引用形参、类的成员、全局变量、静态变量。
- 返回引用的函数是被引用的变量的别名,将const用于引用的返回类型。
9.各种形参的使用
1)如果不需要在函数中修改实参
- 如果实参很小,如C++内置的数据类型或小型结构体,则按值传递。
- 如果实参是数组,则使用const指针,因为这是唯一的选择(没有为数组建立引用的说法)。
- 如果实参是较大的结构,则使用const指针或const引用。
- 如果实参是类,则使用const引用,传递类的标准方式是按引用传递(类设计的语义经常要求使用引用)。
2)如果需要在函数中修改实参
- 如果实参是内置数据类型,则使用指针。只要看到func(&x)的调用,表示函数将修改x。
- 如果实参是数组,则只能使用指针。
- 如果实参是结构体,则使用指针或引用。
- 如果实参是类,则使用引用。