C++ 语法基础(三)

指针

知识点

指针的概念

指针保存了一个内存地址,对指针的操作就是对地址的操作。

可以将内存理解为一个“大数组”,指针相当于存储了一个数组下标,它指向下标对应位置的变量。

指针的声明

用以下代码声明指针。其中,&运算符被称为取地址运算符,它返回变量在内存中的地址。

void指针可以指向任意类型的值。

int a;
int *ptrToA = &a;

double b;
double *ptrToB = &b;

void *voidPtrToA = &a;
void *voidPtrToB = &b;

指针的声明

用以下代码声明指针。其中,&运算符被称为取地址运算符,它返回变量在内存中的地址。

void指针可以指向任意类型的值。

int a;
int *ptrToA = &a;

double b;
double *ptrToB = &b;

void *voidPtrToA = &a;
void *voidPtrToB = &b;

取地址运算符不能作用于常量或表达式,如 int *ptr = &2; 或者 int *ptr = &(a + b); 。因为他们在内存中并没有固定的地址。

空指针

空指针的值可以用 NULL (C和C++98的风格)或者 nullptr (C++11新标准)表示。他们的值都是0x0,表示指针不指向任何对象。

空指针不可以解引用,对其解引用会出现运行时错误(Runtime Error)。

NULL的定义:

//cite from <stddef.h>
#ifndef __cplusplus
#define NULL ((void *)0) // C语言
#else
#define NULL 0 // C++
#endif

可以发现,C语言的NULL保证为 void* 类型,但C++的NULL仅为常量 0。在C++中使用NULL宏可能会导致函数重载错误。相比之下, C++11引入的 nullptr 始终保证其为指针类型。

在C++代码中,建议使用 nullptr

 

知识点

指针赋值

使用指针时,假设 ptr 为一个指针。

  1. ptr 的值为指针本身的值,是一个十六进制地址;
  2. 同类型的指针之间可以赋值,如 ptr1 = ptr2,赋值相当于改变了指针指向的对象。

间接访问

为了访问指针指向的值,我们使用 * 符号,这被称为解引用运算符:

  1. *ptr 的值为指针指向的变量的值;
  2. 对 *ptr 的修改会作用到原对象上。

注意声明int *ptr = &a; 中的 * 并不是解引用运算符,它是类型声明的一部分。(非常非常非常重要的一句话,让我搞错了好长时间)

void类型

void* 指针可以指向任何类型的值。

void* 类型不可以解引用。

事实上,使用 void* 代表着你放弃了所有类型检查和类型安全性。因此,除非必要,不建议在C++代码中使用 void* 类型。此后我们会学到处理任意类型更强大、安全的工具:模板。

我们看一下以下代码的运行结果,加深一下理解:

 

int a = 233;
int *ptrToA = &a;
void *voidPtrToA = &a;

cout << *ptrToA << endl; // 233
cout << ptrToA << endl; //一个十六进制数,表示内存中的位置。例如0x7ffd99314e64

*ptrToA = 466;
cout << a << endl; // 466

*voidPtrToA = 699; // Compile Error: ‘void*’ is not a pointer-to-object type

int c = 1, *ptrToC = &c;
ptrToA = ptrToC; // 现在ptrToA指向了变量c

reinterpret_cast

对于一个指向类型 A 的指针,我们可以将其转换成一个指向类型 B 的指针。此时,指针指向的位置没有变,只是对于内存中数据的解释方式变了。

转换的方式便是 B *ptr2 = reinterpret_cast<B *> ptr1;

如以下代码,表示用一个 float 指针解释一个内存中的 int 变量:

int x = 1;
float *fp = reinterpret_cast<float *> &x; 

要注意的是转换后的类型,它的有效长度不能比原来的类型更长。比如说 int 类型为 4 byte,double 类型为 8 byte。将一个指向 int 类型的指针转换成指向 double 类型的指针,这在语法上没有问题,但是如果解引用得到的指针,double 多出的 4 byte 的数据是无意义的。 

 

 

 

 

指针数组

知识点

数组名其实就是指向数组第0个元素的指针。但是,不能修改“数组名”这个指针的值,即它是常量指针。

int a[] = {1, 2, 3};
*a; // 等同于a[0]
*(a + n); // 等同于a[n]

int *p = a + 1; 
*p; // 2
*(p + 1); // 3

 

 

知识点

当指针指向数组元素时,加减法才有意义。

可以对指针加上或者减去一个整数。这表示:将指针在数组中向前或向后移动若干位置。

当两个指针指向同一个数组时,可以对两个指针做减法。这表示两个指针所指向元素在数组中的距离。

 

p = p + 1; // *p == 3
p = p - 1; // *p == 2
cout << p - a << endl; // 1

指针在偏移后不能超过数组的范围。当对超过数组范围的指针解引用时,行为未定义(可能出现运行时错误)。

同样,也不要对不在同一个数组内的两个指针执行减法。

 

//题目描述:
//实现函数`productMinMax`,返回数组中最小值和最大值的积。
//保证结果不会超过int范围。
//输入:无
//输出:`-90`
#include <iostream>
int productMinMax(int *arr, int len)
{
    int minV = *arr, maxV = *arr;
    for (int *p = arr + 1; p < arr + len; ++p)
    {
        if (minV > *p) minV = *p;
        if (maxV < *p) maxV = *p;
    }
    return minV * maxV;
}
int main()
{
    int arr[] {1, 5, -4, 3, 2, 10, -3, -9, -8};
    std::cout << productMinMax(arr, sizeof(arr) / sizeof(int)) << std::endl;
}

动态内存分配

知识点

new Type在堆(程序的静态存储区)上新建一个对象,返回指向这个对象的指针。

new Type[cnt] 在堆上新建一个长为cnt,类型为Type的数组,返回这个数组的首指针。

new运算符的初始化:

int *p1 = new int; // *p1为不确定的任意值
int *p2 = new int(3); // *p1 == 3
int *p3 = new int(); // *p1 被初始化为0

int *p4 = new int[4]; // p4数组的所有元素为不确定值
int *p5 = new int[4] {1, 2, 3, 4}; // 用花括号列表初始化
int *p6 = new int[4] (); // p6数组的所有元素被初始化为0
int *p7 = new int[4] {1}; // 注意:p7数组仅有第0个元素为1;其余元素都被初始化为0

无论如何,不建议依赖上文中“初始化为0”的语法,这会增加debug的难度。所有new得到的元素应该被显式初始化。

delete ptr 和 delete ptr[] 可以释放ptr对应的内存。

  • 如果ptr并不指向 new 的内存,程序会发生运行时错误
  • 如果已经 new 的内存未被 delete,则发生内存泄漏

当new操作失败(如系统内存耗尽),它返回nullptr。使用new时建议检测是否成功。

 

动态变量实例

知识点

使用传统的数组,我们只能在源代码中就写好数组的大小,不能修改;

通过申请动态数组,我们可以在运行时控制数组的大小。

指针与字符串 

知识点

字符串也是储存在数组中的。可以把字符串常量赋值给数组来初始化。

由于字符串的最后一个字节一定为'\0',只要得到数组的首指针,就可以遍历整个字符串。

char ss[] = "abc", *str = ss; 

char *str1 = new char[10]; 
strcpy(str1, "abc");

//输出ss,等价于cout << ss << endl;
for (char *p = ss; *p != 0; ++p) 
{
    cout << *p;
}
cout << endl;

也可以把字符串常量赋给 const char* 指针。因为字符串常量的每一个字符不可以修改,使用了 const 修饰符。但这是较陈旧的用法,不建议使用。如果不使用 const,严格说是错误的(编译器会提示warning,但可以运行),此时若修改字符串的任一位,行为未定义(undefined)。

const char *s1 = "hello"; // ok, deprecated
char *s2 = "world"; // warning: ISO C++ forbids converting a string constant to ‘char*’
//题目描述:
//实现函数`myStrcmp`,比较两个输入的字符串。
//如果第一个字符串字典序小于第二个,返回-1;如果字典序相同,返回0;如果第一个字符串字典序大于第二个,返回1。
//输入:无
//输出:`-1`

#include <iostream>
int myStrcmp(const char *s1, const char *s2)
{
    while (*s1 && *s2)
    {
        if (*s1 < *s2)
        {
            return -1;
        } 
        else if (*s1 > *s2)
        {
            return 1;
        }
        s1++;
        s2++;
    }
    if (*s1)
    {
        return 1;
    } 
    else if (*s2)
    {
        return -1;
    } 
    else
    {
        return 0;
    }
}
int main()
{
    char s1[] = "abcde", s2[] = "abcef";
    std::cout << myStrcmp(s1, s2) << std::endl;
}

指针与函数

知识点

如何在函数中修改传入变量的值?一种常用的方法是使用指针。

void swap(int *a, int *b) 
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

int main() 
{
    int a = 1, b = 2;
    swap(&a, &b);
}

 

 求解一元二次方程的函数

知识点

 

//题目描述:
//请实现函数 `int SolveQuadratic(double a, double b, double c, double *px1,  double *px2)`,返回一元二次方程 ax^2 + bx + c = 0 的两个根,并返回不同的根的个数。如果 a=0,返回3。
//输入:无
//输出:
//2
//2 1
#include <iostream>
int SolveQuadratic(double a, double b, double c, double *px1, double *px2)	
{ 
	double disc, sqrtDisc;
	if(a == 0) return 3;
	disc = b * b - 4 * a * c;
	if( disc < 0 ) return 0;
	if ( disc == 0 ) { *px1 = -b /(2 * a); return 1;}
	sqrtDisc = sqrt(disc);
	*px1 = (-b + sqrtDisc) / (2 * a);
	*px2 = (-b - sqrtDisc) / (2 * a);
	return 2;
}

int main()
{
	double x1, x2;
	std::cout << SolveQuadratic(1, -3, 2, &x1, &x2) << std::endl;
	std::cout << x1 << " " << x2 << std::endl;
}

 返回指针的函数

知识点

指向全局变量的指针可以作为函数返回值。

但局部变量不可以。局部变量的生命周期为:声明时,直到当前作用域(花括号)结束。函数返回后,该局部变量对应内存被回收,指向它的指针无意义。

指向new得到的内存的指针可以作为返回值。该内存在堆上分配,直到被delete之前都不会被回收。

int tmp;
int *func1 (int val) 
{
    int a;
    int *b = new int [2];
    // return &val; // err: val的生命周期在return时就结束了
    // return &a; // err: 同上
    // return b; // ok
    return &tmp; // ok
}

 

引用概念

知识点

引用可以理解为变量的"别名"。同时,也可以理解为一个type *const指针,即指针指向的对象的值可变,但指针本身的地址不可变。在指针的基础上,引用省略了取地址和解引用。

对引用的操作(求值,修改等)始终绑定在原对象上。

int a = 1;
int &b = a; // a == 1; b == 1
a = 2; // a == 2; b == 2
b = 3; // a == 3; b == 3

cout << sizeof(b) << endl; // 4; 和sizeof(a)相同
cout << &b << ' ' << &a << ' ' << (&a == &b) << endl; // true; a和b的地址是一样的

int *c = &b; // 此时c指向a
cout << sizeof(c) << endl; // 64位系统上为8; 和上面sizeof(b)做对比

 

引用传递

知识点

如果想在函数内修改一个对象(而非数组)的值,传引用是更现代的方式。

引用的另一个作用是可以让函数返回多个值。此时,只要传入多个引用,然后把他们当作返回值修改即可。

void swap(int &a, int &b) 
{
    int tmp = a;
    a = b;
    b = tmp;
}

void return2value(int &a, int &b) 
{ //returns {3, 4}
    a = 3, b = 4;
}

int main() 
{
    int a, b;
    return2value(a, b);
    swap(a, b);
    cout << a << ' ' << b << '\n';
}

即使不需要修改传入对象的值,按引用传递往往也是更高效的方式。此时,常常使用常量引用传递。

这对于大对象的传递尤其有效。按值传递大对象时,会导致对整个对象的拷贝,可能很慢;而按引用传递时,开销仅仅为传一个指针(64 位计算机中为 8 bytes)。

因此,大多数时候,C++的函数参数为常量引用或普通引用,非引用/指针的参数出现较少。

 

// struct的知识会在后面学到,现在只需要知道BigType是个很大的对象: sizeof(BigType) == 4000
struct BigType 
{
    int val[1000];
};
void func1(BigType a) // copies 4000 bytes
{ 
    // ...
}
void func2(const BigType &a) // copies 8 bytes
{ 
    // ...
}
//题目描述:
//实现函数`myFindCnt`,返回`vector`数组中某个值`val`第一次出现的下标。
//如果该值不存在,返回数组的长度。
//同时,要在函数的cnt参数中返回这个值出现的次数。
//输入:无
//输出:
//4
//2
#include <iostream>
#include <vector>
using namespace std;
int myFindCnt(const vector<int> &arr, int val, int &cnt)
{
    int res = arr.size();
    cnt = 0;
    for (int i = 0; i < arr.size(); ++i)
    {
        if (arr[i] == val)
        {
            if (res == arr.size())
            {
                res = i;
            }
            ++cnt;
        }
    }
    return res;
}
int main()
{
    vector<int> arr = {1, 2, 6, 1, 3, 4, 2, 4, 3};
    int cnt;
    std::cout << myFindCnt(arr, 3, cnt) << std::endl;
    std::cout << cnt << std::endl;
}

知识点

函数也可以返回变量的引用,此时和指针类似。可以返回的引用包括:

  • 对全局变量的引用
  • 返回函数的引用类型参数
  • 返回函数指针类型参数解引用之后的结果

同上,不能返回局部变量的引用。

int glob;
int& func(int a, int &b, int *c) 
{
    int tmp;
    return a; // err
    return tmp; // err
    return b; // ok
    return *c; // ok: *c为引用类型
    return glob; // ok
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
C++是一种通用的编程语言,它支持面向对象的编程风格,并且具有强大的系统编程能力。下面是C++的一些基础语法: 1. 注释:在C++中,注释可以用来解释代码,以便于其他人阅读和理解。单行注释使用双斜线(//),多行注释使用斜线和星号(/* ... */)。 2. 标识符:标识符是变量、函数、类等的名称。标识符由字母、数字和下划线组成,并且以字母或下划线开头。 3. 变量:在C++中,需要声明变量来存储数据。变量的声明包括类型和名称。例如,int表示整数类型,而float表示浮点数类型。 4. 数据类型:C++提供了多种数据类型,包括整型(int、short、long)、浮点型(float、double)、字符型(char)、布尔型(bool)等。 5. 运算符:C++支持各种运算符,例如算术运算符(+、-、*、/)、关系运算符(==、!=、<、>)、逻辑运算符(&&、||、!)等。 6. 控制流语句:C++提供了多种控制流语句,例如条件语句(if-else)、循环语句(for、while、do-while)、跳转语句(break、continue、return)等。 7. 函数:函数是可重用的代码块,用于执行特定的任务。函数由函数头和函数体组成,函数头包括返回类型、函数名和参数列表。 8. 类和对象:C++是面向对象的语言,支持类和对象的概念。类是一种用户定义的数据类型,用于封装数据和方法。对象是类的实例,可以通过调用对象的方法来操作数据。 这只是C++语言的一些基础语法,还有很多其他的概念和特性。如果你对某个特定的主题有更深入的兴趣,我可以为你提供更详细的信息。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小馨馨的小翟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值