第一章:C++和标准库速读(3)

         const的多种用法
        const在c++有几种不同的用法。这些用法是相关的,但也有些许不同。const的微妙之处为面试提供了绝佳的问题!11章详细介绍const的各种用法。这一节概述一下两种常用的用法。

 

       const常量
       如果你认为const关键 字与常量存在某种联系,那么你已经发现了它的一种用法。在C语言中,程序员经常使用预编译器#define机制来声明程序运行期内不可改变值的符号名称,如版本号。在C++中鼓励程序员使用const替代#define来定义常量。使用const定义常量就像定义一个变量一样,编译器保证程序不会改变它的值。
 
const int versionNumberMajor = 2;
const int versionNumberMinor = 1;
const std::string productName = "Super Hyper Net Modulator";

      const用于保护参数

      C++中非const变量可以转换成const变量。为什么要这样做?它可以提供一定程度上的保护,避免其它代码改变变量。如果调用一个由同事写的函数,并且为了保证这个函数不会改变传递进去的参数,可以告诉同事让这个函数携带常量参数。当函数试图改变参数的值时,代码编译不过。如下的代码中,调用mysteryFunction()函数时string*会自动转换为const string*。如果mysteryFunction()的编写者试图修改传递进来的字符串,代码将编译不过。有几种办法可以避开这种限制,但是使用这些方法需要清醒的努力。C++只保护常量不被意外的修改。

void mysteryFunction(const std::string* someString)
{
    *someString = "Test"; // Will not compile.
}
int main()
{
    std::string myString = "The string";
    mysteryFunction(&myString);
    return 0;
}

        引用

        在C++中引用可以给现有变量取另一个名称。例如:

int x = 42;
int& xReference = x;

       将&和类型放一起来表明这个变量是一个引用。引用仍像普通变量一样使用,但在幕后,它实际上是一个指向原来变量的指针。变量x和引用变量xReference 两者都精确的指向相同的值。如果通过其中一个变量来改变这个值,另一个变量可以看到这种改变。

        引用传递

        通常,将变量传递给函数是按值传递。如果函数带一个整型参数,它确实是你传递的整型的一个拷贝,所以无法修改原来变量的值。在C中指向栈变量的指针经常被用来让函数修改另一个栈帧中的变量。通过解引用,即使变量不在当前栈帧中,也可以使函数能够修改代表变量的内存。这种方法的问题在于一个非常简单的任务却混入了指针语法。C++提供了一个更好的机制,叫做引用传递,来替代向函数传递指针。引用传递用引用参数替代指针。下面是addOne()函数的两种实现。第一种对传递进来的参数没有影响,因为它是按值传递,所以这个函数接收的是传递参数的一个拷贝。第二种使用引用,因此可以改变原来的变量。

void addOne(int i)
{
    i++; // Has no real effect because this is a copy of the original
}
void addOne(int& i)
{
    i++; // Actually changes the original variable
}

        调用整型引用版本函数的语法跟调用整型版本函数的语法没有差别:

int myInt = 7;
addOne(myInt);

         注意 addOne()的两种实现有一个微妙的差异。按值传递的版本在接收字面量时没有总是;如,“addOne(3);” 是合法的。然而,按引用传递的版本这样做会导致编译错误。可以通过使用const引用来解决,下一节介绍;也可以用右值引用来解决,这是C++的高级特性,会在第9章介绍。

        如果一个函数需要返回一个大的结构或者类(本章稍后讨论),拷贝它们的代价是昂贵的,所以函数经常非常量引用这类结构体或者类,然后修改它们来替代直接返回。为了避免从函数返回结构体或者类时创建一个拷贝所带来的性能开销,这在很久之前是推荐作法。从C++11开始,这已不在是必须的了。多亏move语法,从函数直接返回结构体或类是高效的,不再需要拷贝。在第9章中详细讨论move语法。

 

        常引用传递
         函数经常使用const引用参数。起初这看起来像是一个矛盾。引用参数可以改变另一个上下文中的变量。const却阻止这种改变。const引用参数的主要价值在于效率。当向函数传递值时会发生整体的拷贝。当传递引用时,仅仅传递一个指向原来变量的指针,这样编译器就不用拷贝了。通过传递const引用便获得了两方面的好处:没有拷贝,且原来的变量也不能被修改。const引用在处理对象时特别重要,因为它们也许很大,拷贝它们会有不良的副作用。像这样微妙的问题在第11章中有介绍。下面的例子展示了如何将std::string作为const引用传递给函数:
 
void printString(const std::string& myString)
{
    std::cout << myString << std::endl;
}
int main()
{
    std::string someString = "Hello World";
    printString(someString);
    printString("Hello World"); // Passing literals works
    return 0;
}

 

Exceptions
C++ is a very flexible language, but not a particularly safe one. The compiler will let you write code
that scribbles on random memory addresses or tries to divide by zero (computers don’t deal well
with infinity). One language feature that attempts to add a degree of safety back to the language is
exceptions.
An exception is an unexpected situation. For example, if you are writing a function that retrieves
a web page, several things could go wrong. The Internet host that contains the page might be
down, the page might come back blank, or the connection could be lost. One way you could handle
this situation is by returning a special value from the function, such as nullptr or an error code.
Exceptions provide a much better mechanism for dealing with problems.
Exceptions come with some new terminology. When a piece of code detects an exceptional situation,
it throws an exception. Another piece of code catches the exception and takes appropriate action.
The following example shows a function, divideNumbers() , that throws an exception if the caller
passes in a denominator of zero. The use of std::invalid_argument requires <stdexcept> .
double divideNumbers(double numerator, double denominator)
{
    if (denominator == 0) {
        throw invalid_argument("Denominator cannot be 0.");
    }
    return numerator / denominator;
}

 

When the throw line is executed, the function immediately ends without returning a value. If the
caller surrounds the function call with a try/catch block, as shown in the following code, it
receives the exception and is able to handle it:
try {
    cout << divideNumbers(2.5, 0.5) << endl;
    cout << divideNumbers(2.3, 0) << endl;
    cout << divideNumbers(4.5, 2.5) << endl;
} catch (const invalid_argument& exception) {
    cout << "Exception caught: " << exception.what() << endl;
}

 

The first call to divideNumbers() executes successfully, and the result is output to the user. The
second call throws an exception. No value is returned, and the only output is the error message that
is printed when the exception is caught. The third call is never executed because the second call
throws an exception, causing the program to jump to the catch block. The output for the preceding
block of code is as follows:
5
An exception was caught: Denominator cannot be 0.
Exceptions can get tricky in C++. To use exceptions properly, you need to understand what happens
to the stack variables when an exception is thrown, and you have to be careful to properly catch
and handle the necessary exceptions. Also, the preceding example uses the built-in std::invalid_
argument type, but it is preferable to write your own exception types that are more specific to the
error being thrown. Lastly, the C++ compiler doesn’t force you to catch every exception that might
occur. If your code never catches any exceptions but an exception is thrown, it will be caught by the
program itself, which will be terminated. These trickier aspects of exceptions are covered in much
more detail in Chapter 14.
Type Inference
Type inference allows the compiler to automatically deduce the type of an expression. There are two
keywords for type inference: auto and decltype .
The auto Keyword
The auto keyword has a number of completely different uses:
Deducing a function’s return type, as explained earlier in this chapter.
Structured bindings, as explained earlier in this chapter.
Deducing the type of an expression, as discussed later in this section.
Deducing the type of non-type template parameters, see Chapter 12.
decltype(auto) , see Chapter 12.
Alternative function syntax, see Chapter 12.
Generic lambda expressions, see Chapter 18.
auto can be used to tell the compiler to automatically deduce the type of a variable at compile time.
The following line shows the simplest use of the auto keyword in that context:
auto x = 123; // x will be of type int

 

In this example, you don’t win much by typing auto instead of int ; however, it becomes useful
for more complicated types. Suppose you have a function called getFoo() that has a complicated
return type. If you want to assign the result of calling getFoo() to a variable, you can spell out the
complicated type, or you can simply use
auto and let the compiler figure it out:
auto result = getFoo();

 

This has the added benefit that you can easily change the function’s return type without having to
update all the places in the code where that function is called.
However, using auto to deduce the type of an expression strips away reference and const qualifiers.
Suppose you have the following function:
#include <string>
const std::string message = "Test";
const std::string& foo()
{
    return message;
}

 

You can call foo() and store the result in a variable with the type specified as auto , as follows:
auto f1 = foo();

 

Because auto strips away reference and const qualifiers, f1 is of type string , and thus a copy is
made. If you want a const reference, you can explicitly make it a reference and mark it const , as
follows:
const auto& f2 = foo();
WARNING Always keep in mind that auto strips away reference and const
qualifiers, and thus creates a copy! If you do not want a copy, use auto& or
const auto& .

 

The decltype Keyword
The decltype keyword takes an expression as argument, and computes the type of that expression,
as shown here:
int x = 123;
decltype(x) y = 456;

 

In this example, the compiler deduces the type of y to be int because that is the type of x .
The difference between auto and decltype is that decltype does not strip reference and const
qualifiers. Take again the function foo() returning a const reference to a string . Defining f2
using decltype as follows results in f2 being of type const string& , and thus no copy is made.
decltype(foo()) f2 = foo();

 

On first sight, decltype doesn’t seem to add much value. However, it is pretty powerful in the con
text of templates, discussed in Chapters 12 and 22.

 

C++ AS AN OBJECT-ORIENTED LANGUAGE
If you are a C programmer, you may have viewed the features covered so far in this chapter as con
venient additions to the C language. As the name C++ implies, in many ways the language is just a
“better C.” There is one major point that this view overlooks: unlike C, C++ is an object-oriented
language.
Object-oriented programming (OOP) is a very different, arguably more natural, way to write code.
If you are used to procedural languages such as C or Pascal, don’t worry. Chapter 5 covers all the
background information you need to know to shift your mindset to the object-oriented paradigm.
If you already know the theory of OOP, the rest of this section will get you up to speed (or refresh
your memory) on basic C++ object syntax.
Defining Classes
A class defines the characteristics of an object. In C++, classes are usually defined in a header file
(.h), while their definitions usually are in a corresponding source file (.cpp).
A basic class definition for an airline ticket class is shown in the following example. The class can
calculate the price of the ticket based on the number of miles in the flight and whether or not the
customer is a member of the “Elite Super Rewards Program.” The definition begins by declaring the
class name. Inside a set of curly braces, the data members (properties) of the class and its methods
(behaviors) are declared. Each data member and method is associated with a particular access level:
public , protected , or private . These labels can occur in any order and can be repeated. Members
that are public can be accessed from outside the class, while members that are private cannot be
accessed from outside the class. It’s recommended to make all your data members private , and if
needed, to give access to them with public getters and setters. This way, you can easily change the
representation of your data while keeping the public interface the same. The use of protected is
explained in the context of inheritance in Chapters 5 and 10.
#include <string>
class AirlineTicket
{
public:
    AirlineTicket();
    ~AirlineTicket();
    double calculatePriceInDollars() const;
    const std::string& getPassengerName() const;
    void setPassengerName(const std::string& name);
    int getNumberOfMiles() const;
    void setNumberOfMiles(int miles);
    bool hasEliteSuperRewardsStatus() const;
    void setHasEliteSuperRewardsStatus(bool status);
private:
    std::string mPassengerName;
    int mNumberOfMiles;
    bool mHasEliteSuperRewardsStatus;
};

 

This book follows the convention to prefix each data member of a class with a lowercase ‘m’, such as
mPassengerName .
NOTE To follow the const -correctness principle, it’s always a good idea to
declare member functions that do not change any data member of the object as
being const . These member functions are also called “inspectors,” compared to
“mutators” for non- const member functions.
The method that has the same name as the class with no return type is a constructor . It is automati
cally called when an object of the class is created. The method with a tilde (~) character followed by
the class name is a destructor . It is automatically called when the object is destroyed.
There are two ways of initializing data members with a constructor. The recommended way is
to use a constructor initializer , which follows a colon after the constructor name. Here is the
AirlineTicket constructor with a constructor initializer:
AirlineTicket::AirlineTicket()
: mPassengerName("Unknown Passenger")
, mNumberOfMiles(0)
, mHasEliteSuperRewardsStatus(false)
{
}

A second way is to put the initializations in the body of the constructor, as shown here:

AirlineTicket::AirlineTicket()
{
    // Initialize data members
    mPassengerName = "Unknown Passenger";
    mNumberOfMiles = 0;
    mHasEliteSuperRewardsStatus = false;
}

If the constructor is only initializing data members without doing anything else, then there is no real

need for a constructor because data members can be initialized directly inside the class definition.
For example, instead of writing an AirlineTicket constructor, you can modify the definition of the
data members in the class definition as follows:
private:
std::string mPassengerName = "Unknown Passenger";
int mNumberOfMiles = 0;
bool mHasEliteSuperRewardsStatus = false;

 

If your class additionally needs to perform some other types of initialization, such as opening a file,
allocating memory, and so on, then you still need to write a constructor to handle those.
Here is the destructor for the AirlineTicket class:
AirlineTicket::~AirlineTicket()
{
    // Nothing much to do in terms of cleanup
}

 

This destructor doesn’t do anything, and can simply be removed from this class. It is just shown
here so you know the syntax of destructors. Destructors are required if you need to perform some
cleanup, such as closing files, freeing memory, and so on. Chapters 8 and 9 discuss destructors in
more detail.
The definitions of some of the AirlineTicket class methods are shown here:
double AirlineTicket::calculatePriceInDollars() const
{
    if (hasEliteSuperRewardsStatus()) {
         // Elite Super Rewards customers fly for free!
         return 0;
    }
    // The cost of the ticket is the number of miles times 0.1.
    // Real airlines probably have a more complicated formula!
    return getNumberOfMiles() * 0.1;
}
const string& AirlineTicket::getPassengerName() const
{
    return mPassengerName;
}
void AirlineTicket::setPassengerName(const string& name)
{
    mPassengerName = name;
}

 

 

Using Classes
The following sample program makes use of the AirlineTicket class. This example shows the
creation of a stack-based AirlineTicket object as well as a heap-based one:
AirlineTicket myTicket; // Stack-based AirlineTicket
myTicket.setPassengerName("Sherman T. Socketwrench");
myTicket.setNumberOfMiles(700);
double cost = myTicket.calculatePriceInDollars();
cout << "This ticket will cost $" << cost << endl;
// Heap-based AirlineTicket with smart pointer
auto myTicket2 = make_unique<AirlineTicket>();
myTicket2->setPassengerName("Laudimore M. Hallidue");
myTicket2->setNumberOfMiles(2000);
myTicket2->setHasEliteSuperRewardsStatus(true);
double cost2 = myTicket2->calculatePriceInDollars();
cout << "This other ticket will cost $" << cost2 << endl;
// No need to delete myTicket2, happens automatically
// Heap-based AirlineTicket without smart pointer (not recommended)
AirlineTicket* myTicket3 = new AirlineTicket();
// ... Use ticket 3
delete myTicket3; // delete the heap object!

 

The preceding example exposes you to the general syntax for creating and using classes. Of course,
there is much more to learn. Chapters 8, 9, and 10 go into more depth about the specific C++ mech
anisms for defining classes.

 

UNIFORM INITIALIZATION
Before C++11, initialization of types was not always uniform. For example, take the following defi
nition of a circle, once as a structure, and once as a class:
struct CircleStruct
{
    int x, y;
    double radius;
};
class CircleClass
{
public:
    CircleClass(int x, int y, double radius): mX(x), mY(y), mRadius(radius) {}
private:
    int mX, mY;
    double mRadius;
};

 

In pre-C++11, initialization of a variable of type CircleStruct and a variable of type CircleClass
looks different:
CircleStruct myCircle1 = {10, 10, 2.5};
CircleClass myCircle2(10, 10, 2.5);

 

For the structure version, you can use the {...} syntax. However, for the class version, you need to
call the constructor using function notation (...) .
Since C++11, you can more uniformly use the {...} syntax to initialize types, as follows:
CircleStruct myCircle3 = {10, 10, 2.5};
CircleClass myCircle4 = {10, 10, 2.5};

 

The definition of myCircle4 automatically calls the constructor of CircleClass . Even the use of the
equal sign is optional, so the following is identical:
CircleStruct myCircle5{10, 10, 2.5};
CircleClass myCircle6{10, 10, 2.5};

 

Uniform initialization is not limited to structures and classes. You can use it to initialize anything in
C++. For example, the following code initializes all four variables with the value 3:
int a = 3;
int b(3);
int c = {3}; // Uniform initialization
int d{3}; // Uniform initialization

 

Uniform initialization can be used to perform zero-initialization* of variables; you just specify an empty set of curly braces, as shown here:
int e{}; // Uniform initialization, e will be 0
Using uniform initialization prevents narrowing . C++ implicitly performs narrowing, as shown here:
void func(int i) { /* ... */ }
int main()
{
    int x = 3.14;
    func(3.14);
}

 

In both cases, C++ automatically truncates 3.14 to 3 before assigning it to x or calling func() . Note
that some compilers might issue a warning about this narrowing, while others won’t. With uniform
initialization, both the assignment to x and the call to func() must generate a compiler error if your
compiler fully conforms to the C++11 standard:
void func(int i) { /* ... */ }
int main()
{
    int x = {3.14}; // Error because narrowing
    func({3.14}); // Error because narrowing
}

 

Uniform initialization can be used to initialize dynamically allocated arrays, as shown here:
int* pArray = new int[4]{0, 1, 2, 3};

 

It can also be used in the constructor initializer to initialize arrays that are members of a class.
class MyClass
{
    public:
    MyClass() : mArray{0, 1, 2, 3} {}
    private:
    int mArray[4];
};

 

Uniform initialization can be used with the Standard Library containers as well—such as the
std::vector , as demonstrated later in this chapter.
Direct List Initialization versus Copy List Initialization
There are two types of initialization that use braced initializer lists:
Copy list initialization. T obj = {arg1, arg2, ...};
Direct list initialization. T obj {arg1, arg2, ...};
In combination with auto type deduction, there is an important difference between copy- and direct
list initialization introduced with C++17.
Starting with C++17, you have the following results:
// Copy list initialization
auto a = {11}; // initializer_list<int>
auto b = {11, 22}; // initializer_list<int>
// Direct list initialization
auto c {11}; // int
auto d {11, 22}; // Error, too many elements.

 

Note that for copy list initialization, all the elements in the braced initializer must be of the same
type. For example, the following does not compile:
auto b = {11, 22.33}; // Compilation error

 

In earlier versions of the standard (C++11/14), both copy- and direct list initialization deduce an
initializer_list<> :
// Copy list initialization
auto a = {11}; // initializer_list<int>
auto b = {11, 22}; // initializer_list<int>
// Direct list initialization
auto c {11}; // initializer_list<int>
auto d {11, 22}; // initializer_list<int>

 

THE STANDARD LIBRARY
C++ comes with a Standard Library, which contains a lot of useful classes that can easily be used
in your code. The benefit of using these classes is that you don’t need to reinvent certain classes and
you don’t need to waste time on implementing things that have already been implemented for you.
Another benefit is that the classes available in the Standard Library are heavily tested and verified
for correctness by thousands of users. The Standard Library classes are also tuned for high perfor
mance, so using them will most likely result in better performance compared to making your own
implementation.
A lot of functionality is available to you in the Standard Library. Chapters 16 to 20 provide more
details; however, when you start working with C++ it is a good idea to understand what the
Standard Library can do for you from the very beginning. This is especially important if you are a C
programmer. As a C programmer, you might try to solve problems in C++ the same way you would
solve them in C. However, in C++ there is probably an easier and safer solution to the problem that
involves using Standard Library classes.
You already saw some Standard Library classes earlier in this chapter—for example, std::string ,
std::array , std::vector , std::unique_ptr , and std::shared_ptr . Many more classes are
introduced in Chapters 16 to 20.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要实现在Unity2D中点击某个物体后,物体改变度,并在一段时间后恢复原始度,可以按照以下步骤进行: 1. 创建物体:首先创建一个物体,可以使用Unity的模型编辑器或导入自定义模型。 2. 添加组件:给物体添加刚体组件和碰撞器组件,以便进行物理模拟。 3. 编写脚本:创建一个脚本来控制物体的度变化和复原。在脚本中,可以使用协程来延迟一段时间后恢复原始度。 ```csharp using UnityEngine; public class ObjectController : MonoBehaviour { public float originalSpeed = 5f; // 物体的原始度 public float changedSpeed = 10f; // 物体改变后的度 public float restoreDelay = 2f; // 物体恢复原始度的延迟时间 private Rigidbody2D objectRigidbody; private float currentSpeed; void Start() { objectRigidbody = GetComponent<Rigidbody2D>(); currentSpeed = originalSpeed; } void Update() { // 点击鼠标左键时改变物体度 if (Input.GetMouseButtonDown(0)) { Vector2 mousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition); Collider2D collider = Physics2D.OverlapPoint(mousePosition); if (collider != null && collider.gameObject == gameObject) { ChangeSpeed(changedSpeed); StartCoroutine(RestoreSpeed(restoreDelay)); } } // 应用物体度 Vector2 velocity = transform.up * currentSpeed; objectRigidbody.velocity = velocity; } void ChangeSpeed(float newSpeed) { currentSpeed = newSpeed; } System.Collections.IEnumerator RestoreSpeed(float delay) { yield return new WaitForSeconds(delay); currentSpeed = originalSpeed; } } ``` 4. 在场景中放置物体实例:在场景中放置一个物体的实例,并将物体控制脚本(ObjectController)添加到物体的GameObject上。 通过以上步骤,当点击该物体时,物体的度将改变为指定的度,并在一段时间后恢复原始度。你可以根据需要调整原始度、改变后的度和恢复延迟时间。希望对你有所帮助!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值